summaryrefslogtreecommitdiff
path: root/main/src/ui/java
diff options
context:
space:
mode:
authorcyberta <cyberta@riseup.net>2026-03-09 23:59:50 +0100
committercyberta <cyberta@riseup.net>2026-03-09 23:59:50 +0100
commit733d1ae5bbb5356a228e42a013660743db1c7073 (patch)
tree550f2285ef072e7dcc482d648fccc09222a35791 /main/src/ui/java
parentb1c21e7e1fbc0d09e3d121b89651482a0bb02efd (diff)
parent73ce3c99dde39207990d66c21be8201228cd1f09 (diff)
Merge branch 'schwabe_master' into ssh_new_master
Diffstat (limited to 'main/src/ui/java')
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.kt9
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.kt42
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.kt32
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.kt23
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/core/OpenVPNThreadv3.java72
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java23
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java15
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java7
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt4
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java21
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/MinimalUI.kt365
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/PackageAdapter.kt6
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.kt3
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.kt23
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java2
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java686
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.kt654
17 files changed, 1207 insertions, 780 deletions
diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.kt b/main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.kt
index 9e6dd462..9157c7d2 100644
--- a/main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.kt
+++ b/main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.kt
@@ -11,6 +11,7 @@ import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.Window
+import android.widget.Toast
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
@@ -19,6 +20,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import de.blinkt.openvpn.R
+import de.blinkt.openvpn.core.GlobalPreferences
import de.blinkt.openvpn.core.LocaleHelper
abstract class BaseActivity : AppCompatActivity() {
@@ -28,6 +30,13 @@ abstract class BaseActivity : AppCompatActivity() {
return uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
}
+ protected fun checkMinimalUIDisabled() {
+ if (GlobalPreferences.getMinimalUi()) {
+ Toast.makeText(this, R.string.minimal_ui_not_available, Toast.LENGTH_LONG).show()
+ finish()
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
if (isAndroidTV) {
requestWindowFeature(Window.FEATURE_OPTIONS_PANEL)
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 a80afcab..83841ace 100644
--- a/main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.kt
+++ b/main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.kt
@@ -32,6 +32,8 @@ import de.blinkt.openvpn.R
import de.blinkt.openvpn.VpnProfile
import de.blinkt.openvpn.core.ConfigParser
import de.blinkt.openvpn.core.ConfigParser.ConfigParseError
+import de.blinkt.openvpn.core.GlobalPreferences
+import de.blinkt.openvpn.core.Preferences
import de.blinkt.openvpn.core.ProfileManager
import de.blinkt.openvpn.fragments.Utils
import de.blinkt.openvpn.views.FileSelectLayout
@@ -45,6 +47,7 @@ import java.util.*
class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener {
+ private var initialImportMode: Boolean = false
private var mResult: VpnProfile? = null
@Transient
@@ -64,13 +67,13 @@ class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener
private lateinit var mTLSProfileLabel: TextView
private lateinit var mLogLayout: LinearLayout
private lateinit var mProfilenameLabel: TextView
+ private lateinit var mMakeDefaultProfile: CheckBox
override fun onClick(v: View) {
if (v.id == R.id.fab_save)
userActionSaveProfile()
if (v.id == R.id.permssion_hint && Build.VERSION.SDK_INT == Build.VERSION_CODES.M)
doRequestSDCardPermission(PERMISSION_REQUEST_EMBED_FILES)
-
}
@TargetApi(Build.VERSION_CODES.M)
@@ -153,6 +156,15 @@ class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener
else
saveProfile()
+ if(mMakeDefaultProfile.isChecked || initialImportMode)
+ {
+ val defaultPrefs = Preferences.getDefaultSharedPreferences(this)
+ val editor = defaultPrefs.edit()
+ editor.putString("alwaysOnVpn", mResult!!.uuidString)
+ editor.apply()
+ }
+
+
return true
}
@@ -215,6 +227,7 @@ class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener
ConfigParser.useEmbbedUserAuth(mResult, mEmbeddedPwFile)
vpl.addProfile(mResult)
+ mResult?.addChangeLogEntry("Profile created via ConfigConverter")
ProfileManager.saveProfile(this, mResult)
vpl.saveProfileList(this)
result.putExtra(VpnProfile.EXTRA_PROFILEUUID, mResult!!.uuid.toString())
@@ -640,6 +653,8 @@ class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener
mTLSProfile = findViewById(R.id.tls_profile) as Spinner
mTLSProfileLabel = findViewById(R.id.tls_profile_label) as TextView
+ mMakeDefaultProfile = findViewById(R.id.make_default_profile ) as CheckBox
+
if (savedInstanceState != null && savedInstanceState.containsKey(VPNPROFILE)) {
mResult = savedInstanceState.getSerializable(VPNPROFILE) as VpnProfile?
mAliasName = savedInstanceState.getString("mAliasName")
@@ -797,6 +812,11 @@ class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener
mTLSProfile.visibility = View.VISIBLE
mTLSProfileLabel.visibility = View.VISIBLE
+ mMakeDefaultProfile.visibility = View.VISIBLE
+ if (initialImportMode) {
+ mMakeDefaultProfile.isChecked = true
+ mMakeDefaultProfile.isEnabled = false
+ }
mTLSProfile.setSelection(translateTLSProfileToSelection(result.mTlSCertProfile))
log(R.string.import_done)
@@ -817,12 +837,32 @@ class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener
log("An external app instructed OpenVPN for Android to open a file:// URI. This kind of URI have been deprecated since Android 7 and no longer work on modern Android versions at all.")
doRequestSDCardPermission(PERMISSION_REQUEST_READ_URL)
}
+ }
+
+
+ /**
+ * Checks whether we are in the special mode where we allow an initial profile to be imported but nothing after that
+ */
+ private fun checkInitialImportMode(): Boolean {
+ if (!GlobalPreferences.getMinimalUi())
+ return false
+
+ if (!GlobalPreferences.getAllowInitialImport())
+ return false
+
+ /* We might potentially allow the initial import but only if no profiles exist */
+
+ val defaultProfile = ProfileManager.getAlwaysOnVPN(this)
+ return defaultProfile == null
}
override fun onStart() {
super.onStart()
+ initialImportMode = checkInitialImportMode()
+ if (!initialImportMode)
+ checkMinimalUIDisabled()
}
private fun log(logmessage: String?) {
diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.kt b/main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.kt
index a15de114..d830278e 100644
--- a/main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.kt
+++ b/main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.kt
@@ -14,6 +14,7 @@ import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import de.blinkt.openvpn.R
+import de.blinkt.openvpn.core.GlobalPreferences
import de.blinkt.openvpn.fragments.*
import de.blinkt.openvpn.fragments.ImportRemoteConfig.Companion.newInstance
import de.blinkt.openvpn.views.ScreenSlidePagerAdapter
@@ -22,6 +23,7 @@ class MainActivity : BaseActivity() {
private lateinit var mPager: ViewPager2
private lateinit var mPagerAdapter: ScreenSlidePagerAdapter
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = layoutInflater.inflate(R.layout.main_activity, null)
@@ -32,16 +34,27 @@ class MainActivity : BaseActivity() {
mPagerAdapter = ScreenSlidePagerAdapter(supportFragmentManager, lifecycle, this)
- /* Toolbar and slider should have the same elevation */disableToolbarElevation()
- mPagerAdapter.addTab(R.string.vpn_list_title, VPNProfileList::class.java)
- mPagerAdapter.addTab(R.string.graph, GraphFragment::class.java)
- mPagerAdapter.addTab(R.string.generalsettings, GeneralSettings::class.java)
- mPagerAdapter.addTab(R.string.faq, FaqFragment::class.java)
- if (SendDumpFragment.getLastestDump(this) != null) {
- mPagerAdapter.addTab(R.string.crashdump, SendDumpFragment::class.java)
+ /* Toolbar and slider should have the same elevation */
+ disableToolbarElevation()
+
+ val minimalUi = GlobalPreferences.getMinimalUi();
+ if (isAndroidTV || minimalUi) {
+ mPagerAdapter.addTab(R.string.minimal_ui, MinimalUI::class.java)
}
- if (isAndroidTV)
+ if (!minimalUi) {
+
+ mPagerAdapter.addTab(R.string.vpn_list_title, VPNProfileList::class.java)
+ mPagerAdapter.addTab(R.string.graph, GraphFragment::class.java)
+ mPagerAdapter.addTab(R.string.generalsettings, GeneralSettings::class.java)
+ mPagerAdapter.addTab(R.string.faq, FaqFragment::class.java)
+ if (SendDumpFragment.getLastestDump(this) != null) {
+ mPagerAdapter.addTab(R.string.crashdump, SendDumpFragment::class.java)
+ }
+
+ }
+ if (isAndroidTV || minimalUi)
mPagerAdapter.addTab(R.string.openvpn_log, LogFragment::class.java)
+
mPagerAdapter.addTab(R.string.about, AboutFragment::class.java)
mPager.setAdapter(mPagerAdapter)
@@ -97,7 +110,8 @@ class MainActivity : BaseActivity() {
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.main_menu, menu)
+ if (!GlobalPreferences.getMinimalUi())
+ menuInflater.inflate(R.menu.main_menu, menu)
return super.onCreateOptionsMenu(menu)
}
diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.kt b/main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.kt
index 0dd61748..91806b5e 100644
--- a/main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.kt
+++ b/main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.kt
@@ -20,6 +20,7 @@ import com.google.android.material.tabs.TabLayoutMediator
import de.blinkt.openvpn.R
import de.blinkt.openvpn.VpnProfile
import de.blinkt.openvpn.core.ProfileManager
+import de.blinkt.openvpn.core.VpnStatus
import de.blinkt.openvpn.fragments.Settings_Allowed_Apps
import de.blinkt.openvpn.fragments.Settings_Authentication
import de.blinkt.openvpn.fragments.Settings_Basic
@@ -32,7 +33,7 @@ import de.blinkt.openvpn.fragments.ShowConfigFragment
import de.blinkt.openvpn.fragments.VPNProfileList
import de.blinkt.openvpn.views.ScreenSlidePagerAdapter
-class VPNPreferences : BaseActivity() {
+class VPNPreferences : BaseActivity(), VpnStatus.ProfileNotifyListener {
private var mProfileUUID: String? = null
private var mProfile: VpnProfile? = null
private lateinit var mPager: ViewPager2
@@ -55,7 +56,6 @@ class VPNPreferences : BaseActivity() {
override fun onResume() {
super.onResume()
- profile
// When a profile is deleted from a category fragment in hadset mod we need to finish
// this activity as well when returning
if (mProfile == null || mProfile!!.profileDeleted) {
@@ -87,6 +87,8 @@ class VPNPreferences : BaseActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
+ checkMinimalUIDisabled()
+
mProfileUUID = intent.getStringExtra("$packageName.profileUUID")
if (savedInstanceState != null) {
val savedUUID = savedInstanceState.getString("$packageName.profileUUID")
@@ -100,6 +102,7 @@ class VPNPreferences : BaseActivity() {
finish()
return
}
+ VpnStatus.addProfileStateListener(this);
title = getString(R.string.edit_profile_title, mProfile!!.name)
@@ -208,4 +211,20 @@ class VPNPreferences : BaseActivity() {
Settings_Allowed_Apps::class.java,
)
}
+
+ override fun notifyProfileVersionChanged(
+ uuid: String?,
+ version: Int,
+ changedInThisProcess: Boolean
+ ) {
+ if (mProfile?.uuidString != uuid)
+ return;
+
+ if ((mProfile?.mVersion?: 0) < version)
+ {
+ /* Profile has changed outside of our process. Most likely from the AIDL service. */
+ Toast.makeText(this, R.string.editor_close_profile_changed, Toast.LENGTH_LONG).show();
+ finish();
+ }
+ }
}
diff --git a/main/src/ui/java/de/blinkt/openvpn/core/OpenVPNThreadv3.java b/main/src/ui/java/de/blinkt/openvpn/core/OpenVPNThreadv3.java
index e4d6b32f..3c21e46c 100644
--- a/main/src/ui/java/de/blinkt/openvpn/core/OpenVPNThreadv3.java
+++ b/main/src/ui/java/de/blinkt/openvpn/core/OpenVPNThreadv3.java
@@ -1,12 +1,8 @@
package de.blinkt.openvpn.core;
-import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.provider.Settings;
import android.text.TextUtils;
import net.openvpn.ovpn3.ClientAPI_Config;
@@ -20,16 +16,22 @@ import net.openvpn.ovpn3.ClientAPI_OpenVPNClientHelper;
import net.openvpn.ovpn3.ClientAPI_ProvideCreds;
import net.openvpn.ovpn3.ClientAPI_Status;
import net.openvpn.ovpn3.ClientAPI_TransportStats;
+import net.openvpn.ovpn3.DnsAddress;
+import net.openvpn.ovpn3.DnsDomain;
+import net.openvpn.ovpn3.DnsOptions;
+import net.openvpn.ovpn3.DnsOptions_ServersMap;
+import net.openvpn.ovpn3.DnsServer;
import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
import de.blinkt.openvpn.R;
import de.blinkt.openvpn.VpnProfile;
import static de.blinkt.openvpn.VpnProfile.AUTH_RETRY_NOINTERACT;
-import androidx.annotation.NonNull;
-
public class OpenVPNThreadv3 extends ClientAPI_OpenVPNClient implements Runnable, OpenVPNManagement {
final static long EmulateExcludeRoutes = (1 << 16);
@@ -84,10 +86,56 @@ public class OpenVPNThreadv3 extends ClientAPI_OpenVPNClient implements Runnable
return true;
}
+
@Override
- public boolean tun_builder_add_dns_server(String address, boolean ipv6) {
- mService.addDNS(address);
- return true;
+ public boolean tun_builder_set_dns_options(DnsOptions dns)
+ {
+ boolean dnsadded = false;
+ for(DnsDomain domain:dns.getSearch_domains()) {
+ mService.addSearchDomain(domain.getDomain());
+ }
+
+ /* sort dns server if the provided map is not sorted */
+ TreeMap<Integer, DnsServer> sortedDNSServers = new TreeMap<>(dns.getServers());
+
+ for (Map.Entry<Integer, DnsServer> dnsServerEntry: sortedDNSServers.entrySet() ) {
+ DnsServer server = dnsServerEntry.getValue();
+ int prio = dnsServerEntry.getKey();
+
+ if (DnsServer.Security.Yes.equals(server.getDnssec()))
+ {
+ VpnStatus.logInfo(R.string.dnsserver_ignore_dnnsec, prio, server.to_string().trim());
+ continue;
+ }
+
+ if (!DnsServer.Transport.Plain.equals(server.getTransport()) &&
+ !DnsServer.Transport.Unset.equals(server.getTransport()))
+ {
+ VpnStatus.logInfo(R.string.dnsserver_ignore_tls_doh, prio, server.to_string().trim());
+ continue;
+ }
+
+ for(DnsAddress address: server.getAddresses())
+ {
+ if (address.getPort() == 0 || address.getPort() == 53) {
+ mService.addDNS(address.getAddress());
+ dnsadded = true;
+ }
+ else
+ {
+ VpnStatus.logInfo(R.string.dnsserver_ignore_dnsport,
+ address.getAddress(), address.getPort(), prio, server.to_string().trim());
+ }
+ }
+ /* We apply only the first DNS priority that works for us, so skip the rest after
+ * applying one */
+ if (dnsadded)
+ return true;
+
+ }
+ VpnStatus.logError(R.string.dnsserver_no_valid_server);
+ stopVPN(false);
+ return false;
}
@Override
@@ -114,12 +162,6 @@ public class OpenVPNThreadv3 extends ClientAPI_OpenVPNClient implements Runnable
}
@Override
- public boolean tun_builder_add_search_domain(String domain) {
- mService.setDomain(domain);
- return true;
- }
-
- @Override
public boolean tun_builder_set_proxy_http(String host, int port)
{
return mService.addHttpProxy(host, port);
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java b/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java
index 0e36a133..ab55bebd 100644
--- a/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java
@@ -249,20 +249,15 @@ public class ConnectionsAdapter extends RecyclerView.Adapter<ConnectionsAdapter.
mProxyGroup.setOnCheckedChangeListener((group, checkedId) -> {
if (mConnection != null) {
- switch (checkedId) {
- case R.id.proxy_none:
- mConnection.mProxyType = Connection.ProxyType.NONE;
- break;
- case R.id.proxy_http:
- mConnection.mProxyType = Connection.ProxyType.HTTP;
- break;
- case R.id.proxy_socks:
- mConnection.mProxyType = Connection.ProxyType.SOCKS5;
- break;
- case R.id.proxy_orbot:
- mConnection.mProxyType = Connection.ProxyType.ORBOT;
- break;
- }
+ if (checkedId == R.id.proxy_none)
+ mConnection.mProxyType = Connection.ProxyType.NONE;
+ else if (checkedId == R.id.proxy_http)
+ mConnection.mProxyType = Connection.ProxyType.HTTP;
+ else if (checkedId == R.id.proxy_socks)
+ mConnection.mProxyType = Connection.ProxyType.SOCKS5;
+ else if (checkedId == R.id.proxy_orbot)
+ mConnection.mProxyType = Connection.ProxyType.ORBOT;
+
setVisibilityProxyServer(this, mConnection);
}
});
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java
index 2863b242..5bf5a56a 100644
--- a/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java
@@ -129,20 +129,11 @@ public class FaqFragment extends Fragment {
new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_androids_clients_title, R.string.faq_android_clients),
-
-
- new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, Build.VERSION_CODES.JELLY_BEAN_MR2, R.string.vpn_tethering_title, R.string.faq_tethering),
- new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, Build.VERSION_CODES.JELLY_BEAN_MR2, R.string.broken_images, R.string.broken_images_faq),
-
new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.battery_consumption_title, R.string.baterry_consumption),
- new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, Build.VERSION_CODES.KITKAT, R.string.faq_system_dialogs_title, R.string.faq_system_dialogs),
new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tap_mode, R.string.faq_tap_mode),
- new FAQEntry(Build.VERSION_CODES.JELLY_BEAN_MR2, Build.VERSION_CODES.JELLY_BEAN_MR2, R.string.ab_secondary_users_title, R.string.ab_secondary_users),
- new FAQEntry(Build.VERSION_CODES.JELLY_BEAN_MR2, -1, R.string.faq_vpndialog43_title, R.string.faq_vpndialog43),
-
new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tls_cipher_alert_title, R.string.tls_cipher_alert),
new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_security_title, R.string.faq_security),
@@ -150,15 +141,9 @@ public class FaqFragment extends Fragment {
new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_shortcut, R.string.faq_howto_shortcut),
new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tap_mode, R.string.tap_faq2),
- new FAQEntry(Build.VERSION_CODES.KITKAT, -1, R.string.vpn_tethering_title, R.string.ab_tethering_44),
- new FAQEntry(Build.VERSION_CODES.KITKAT, -441, R.string.ab_kitkat_mss_title, R.string.ab_kitkat_mss),
new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.copying_log_entries, R.string.faq_copying),
- new FAQEntry(Build.VERSION_CODES.KITKAT, -442, R.string.ab_persist_tun_title, R.string.ab_persist_tun),
new FAQEntry(Build.VERSION_CODES.KITKAT, -1, R.string.faq_routing_title, R.string.faq_routing),
- new FAQEntry(Build.VERSION_CODES.KITKAT, Build.VERSION_CODES.KITKAT, R.string.ab_kitkat_reconnect_title, R.string.ab_kitkat_reconnect),
- new FAQEntry(Build.VERSION_CODES.KITKAT, Build.VERSION_CODES.KITKAT, R.string.ab_vpn_reachability_44_title, R.string.ab_vpn_reachability_44),
-
new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.ab_only_cidr_title, R.string.ab_only_cidr),
new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.ab_proxy_title, R.string.ab_proxy),
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java
index 01574f20..496305f8 100644
--- a/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java
@@ -78,13 +78,6 @@ public class FaqViewAdapter extends RecyclerView.Adapter<FaqViewAdapter.FaqViewH
String content = mContext.getString(faqItems[i].description);
mHtmlEntries[i] = Html.fromHtml(textColor + content);
-
- // Add hack R.string.faq_system_dialogs_title -> R.string.faq_system_dialog_xposed
- if (faqItems[i].title == R.string.faq_system_dialogs_title)
- {
- Spanned xPosedtext = Html.fromHtml(textColor + mContext.getString(R.string.faq_system_dialog_xposed));
- mHtmlEntries[i] = (Spanned) TextUtils.concat(mHtmlEntries[i], xPosedtext);
- }
}
}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt
index 8430d788..6d7d96ac 100644
--- a/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt
@@ -88,7 +88,7 @@ internal abstract class KeyChainSettingsFragment : Settings_Fragment(), View.OnC
try {
val b = ExtAuthHelper.getCertificateMetaData(context!!, mProfile.mExternalAuthenticator, mProfile.mAlias)
mProfile.mAlias = b.getString(ExtAuthHelper.EXTRA_ALIAS)
- requireActivity().runOnUiThread { setAlias() }
+ activity?.runOnUiThread { setAlias() }
} catch (e: KeyChainException) {
e.printStackTrace()
}
@@ -138,7 +138,7 @@ internal abstract class KeyChainSettingsFragment : Settings_Fragment(), View.OnC
val certStringCopy = certstr
val finalMetadata = metadata
- requireActivity().runOnUiThread {
+ activity?.runOnUiThread {
mAliasCertificate.text = certStringCopy
if (finalMetadata != null)
mExtAliasName.text = finalMetadata.getString(ExtAuthHelper.EXTRA_DESCRIPTION)
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java
index 4a7cf735..fc9667b3 100644
--- a/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java
@@ -103,17 +103,12 @@ public class LogFragment extends ListFragment implements StateListener, SeekBar.
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
- switch (checkedId) {
- case R.id.radioISO:
- ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_ISO);
- break;
- case R.id.radioNone:
- ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_NONE);
- break;
- case R.id.radioShort:
- ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_SHORT);
- break;
-
+ if (checkedId == R.id.radioISO) {
+ ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_ISO);
+ } else if (checkedId == R.id.radioNone) {
+ ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_NONE);
+ } else if (checkedId == R.id.radioShort) {
+ ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_SHORT);
}
}
@@ -522,10 +517,10 @@ public class LogFragment extends ListFragment implements StateListener, SeekBar.
String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
final VpnProfile profile = ProfileManager.get(getActivity(), configuredVPN);
- ProfileManager.getInstance(getActivity()).saveProfile(getActivity(), profile);
+ ProfileManager.saveProfile(getActivity(), profile);
// Name could be modified, reset List adapter
- AlertDialog.Builder dialog = new AlertDialog.Builder(getActivity());
+ AlertDialog.Builder dialog = new AlertDialog.Builder(requireActivity());
dialog.setTitle(R.string.configuration_changed);
dialog.setMessage(R.string.restart_vpn_after_change);
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/MinimalUI.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/MinimalUI.kt
new file mode 100644
index 00000000..42260bc8
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/MinimalUI.kt
@@ -0,0 +1,365 @@
+/*
+ * Copyright (c) 2012-2025 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.Manifest
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.ActivityNotFoundException
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.os.IBinder
+import android.os.RemoteException
+import android.security.KeyChain
+import android.text.TextUtils
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CompoundButton
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.app.ActivityCompat.invalidateOptionsMenu
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import de.blinkt.openvpn.LaunchVPN
+import de.blinkt.openvpn.R
+import de.blinkt.openvpn.VpnProfile
+import de.blinkt.openvpn.VpnProfile.TYPE_KEYSTORE
+import de.blinkt.openvpn.VpnProfile.TYPE_USERPASS_KEYSTORE
+import de.blinkt.openvpn.activities.BaseActivity
+import de.blinkt.openvpn.activities.ConfigConverter
+import de.blinkt.openvpn.core.ConnectionStatus
+import de.blinkt.openvpn.core.GlobalPreferences
+import de.blinkt.openvpn.core.IOpenVPNServiceInternal
+import de.blinkt.openvpn.core.OpenVPNService
+import de.blinkt.openvpn.core.ProfileManager
+import de.blinkt.openvpn.core.VpnStatus
+import de.blinkt.openvpn.fragments.ImportRemoteConfig.Companion.newInstance
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class MinimalUI: Fragment(), VpnStatus.StateListener {
+ private var mLastConnectionLevel: ConnectionStatus = ConnectionStatus.LEVEL_NOTCONNECTED
+ private var mPermReceiver: ActivityResultLauncher<String>? = null
+ private lateinit var mFileImportReceiver: ActivityResultLauncher<Intent?>
+ private lateinit var profileManger: ProfileManager
+ private var mService: IOpenVPNServiceInternal? = null
+ private lateinit var vpnstatus: TextView
+ private lateinit var vpntoggle: CompoundButton
+
+ private lateinit var view: View
+ private var mImportMenuActive = false
+
+ private val mConnection: ServiceConnection = object : ServiceConnection {
+ override fun onServiceConnected(
+ className: ComponentName,
+ service: IBinder
+ ) {
+ mService = IOpenVPNServiceInternal.Stub.asInterface(service)
+ }
+
+ override fun onServiceDisconnected(arg0: ComponentName) {
+ mService = null
+ }
+ }
+
+ private fun registerPermissionReceiver() {
+ mPermReceiver = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { result: Boolean? ->
+ checkForNotificationPermission(
+ requireView()
+ )
+ }
+ }
+
+ private fun registerStartFileImportReceiver()
+ {
+ mFileImportReceiver = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult())
+ {
+ result: ActivityResult ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val uri = result.data?.data
+ val startImport = Intent(getActivity(), ConfigConverter::class.java)
+ startImport.setAction(ConfigConverter.IMPORT_PROFILE)
+ startImport.setData(uri)
+ startActivity(startImport)
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ registerPermissionReceiver()
+ registerStartFileImportReceiver()
+ setHasOptionsMenu(true)
+
+ profileManger = ProfileManager.getInstance(requireContext());
+
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ if (GlobalPreferences.getAllowInitialImport() && ProfileManager.getAlwaysOnVPN(requireContext()) == null ) {
+ mImportMenuActive = true
+ menu.add(0, MENU_IMPORT_PROFILE, 0, R.string.menu_import)
+ .setIcon(R.drawable.ic_menu_import)
+ .setAlphabeticShortcut('i')
+ .setTitleCondensed(getActivity()!!.getString(R.string.menu_import_short))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
+
+ menu.add(0, MENU_IMPORT_AS, 0, R.string.import_from_as)
+ .setIcon(R.drawable.ic_menu_import_download)
+ .setAlphabeticShortcut('p')
+ .setTitleCondensed("Import AS")
+ .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM)
+ }
+ }
+
+ private fun startASProfileImport(): Boolean {
+ val asImportFrag = newInstance(null)
+ asImportFrag.show(getParentFragmentManager(), "dialog")
+ invalidateOptionsMenu(activity)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ val itemId = item.getItemId()
+
+ if (itemId == MENU_IMPORT_PROFILE) {
+ val intent = Utils.getFilePickerIntent(getActivity()!!, Utils.FileType.OVPN_CONFIG)
+ mFileImportReceiver.launch(intent)
+ invalidateOptionsMenu(activity)
+
+ } else if (itemId == MENU_IMPORT_AS) {
+ return startASProfileImport()
+ }
+
+ return super.onOptionsItemSelected(item)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ VpnStatus.addStateListener(this)
+
+ val intent = Intent(requireActivity(), OpenVPNService::class.java)
+ intent.action = OpenVPNService.START_SERVICE
+ requireActivity().bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
+ if (mImportMenuActive && ProfileManager.getAlwaysOnVPN(requireActivity()) != null )
+ invalidateOptionsMenu(requireActivity())
+ }
+
+ override fun onPause() {
+ super.onPause()
+ VpnStatus.removeStateListener(this)
+
+ requireActivity().unbindService(mConnection)
+ }
+
+ private fun checkForNotificationPermission(v: View) {
+ val permissionView = v.findViewById<View>(R.id.notification_permission) ?: return
+
+ val permissionGranted =
+ requireActivity().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
+
+ permissionView.setVisibility(if (permissionGranted) View.GONE else View.VISIBLE)
+ permissionView.setOnClickListener({ view: View? ->
+ mPermReceiver?.launch(
+ Manifest.permission.POST_NOTIFICATIONS
+ )
+ })
+
+ }
+
+ private suspend fun checkForKeychainPermission(v: View) {
+ val keychainView = v.findViewById<View>(R.id.keychain_notification) ?: return
+
+ val profile = ProfileManager.getAlwaysOnVPN(context)
+
+ var permissionGranted = false
+ withContext(Dispatchers.IO)
+ {
+ permissionGranted = (profile == null || !checkKeychainAccessIsMissing(profile))
+ }
+
+
+ keychainView.setVisibility(if (permissionGranted) View.GONE else View.VISIBLE)
+ keychainView.setOnClickListener({
+
+ try {
+ KeyChain.choosePrivateKeyAlias(requireActivity(),
+ { alias ->
+ // Credential alias selected. Remember the alias selection for future use.
+ profile.mAlias = alias
+ ProfileManager.saveProfile(context, profile)
+ viewLifecycleOwner.lifecycleScope.launch {
+ checkForKeychainPermission(v)
+ }
+ },
+ arrayOf("RSA", "EC"), null,
+ profile.mServerName,
+ -1,
+ profile.mAlias)
+ // alias to preselect, null if unavailable
+ } catch (anf: ActivityNotFoundException) {
+ val ab = AlertDialog.Builder(activity)
+ ab.setTitle(R.string.broken_image_cert_title)
+ ab.setMessage(R.string.broken_image_cert)
+ ab.setPositiveButton(android.R.string.ok, null)
+ ab.show()
+ }
+ })
+ }
+
+ override fun updateState(
+ state: String?,
+ logmessage: String?,
+ localizedResId: Int,
+ level: ConnectionStatus,
+ Intent: Intent?
+ ) {
+ val cleanLogMessage = VpnStatus.getLastCleanLogMessage(activity, true)
+
+ requireActivity().runOnUiThread {
+ vpnstatus.setText(cleanLogMessage)
+ val connected = level == ConnectionStatus.LEVEL_CONNECTED;
+ vpntoggle.isChecked = connected
+ }
+ mLastConnectionLevel = level;
+ }
+
+ override fun setConnectedVPN(uuid: String?) {
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ view = inflater.inflate(R.layout.minimalui, container, false)
+ vpntoggle = view.findViewById(R.id.vpntoggle)
+ vpnstatus = view.findViewById(R.id.vpnstatus)
+
+ vpntoggle.setOnClickListener { view ->
+ toggleSwitchPressed(view as CompoundButton)
+ }
+ if ((activity as BaseActivity).isAndroidTV)
+ {
+ with( view.findViewById<TextView>(R.id.minimal_ui_title)) {
+ setOnClickListener { _ -> toggleSwitchPressed(vpntoggle) }
+ visibility = View.VISIBLE;
+ }
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ checkForNotificationPermission(view)
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ checkForKeychainPermission(view)
+ }
+ view.setOnKeyListener { v, key, event ->
+ Toast.makeText(activity, "Got key event " + event + " key " + key + " view " + v, Toast.LENGTH_LONG).show();
+ false;
+ }
+
+ return view
+ }
+
+ fun checkKeychainAccessIsMissing(vp: VpnProfile): Boolean {
+ if ((vp.mAuthenticationType != TYPE_USERPASS_KEYSTORE) && (vp.mAuthenticationType != TYPE_KEYSTORE)) {
+ return false
+ }
+
+ if (TextUtils.isEmpty(vp.mAlias))
+ return true
+ val certs = vp.getExternalCertificates(context)
+ if (certs == null)
+ return true
+
+ return false
+ }
+
+ suspend fun checkVpnConfigured(): VpnProfile? {
+ val alwaysOnVPN = ProfileManager.getAlwaysOnVPN(requireContext())
+ if (alwaysOnVPN == null) {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(
+ requireContext(),
+ R.string.cannot_start_vpn_not_configured,
+ Toast.LENGTH_SHORT
+ ).show();
+ }
+ return null
+ }
+
+ if (checkKeychainAccessIsMissing(alwaysOnVPN))
+ {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(
+ requireContext(),
+ R.string.keychain_access,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ return null
+ }
+ return alwaysOnVPN
+ }
+
+ fun toggleSwitchPressed(view: CompoundButton) {
+ viewLifecycleOwner.lifecycleScope.launch {toggleSwitchPressedReal(view) }
+ }
+
+ suspend fun toggleSwitchPressedReal(view: CompoundButton) {
+ var alwaysOnVPN: VpnProfile? = null
+ withContext(Dispatchers.IO) {
+ alwaysOnVPN = checkVpnConfigured()
+ }
+
+ if (alwaysOnVPN == null)
+ {
+ view.setChecked(false)
+ return
+ }
+
+ // Figure out if we should disconnect
+ if (!GlobalPreferences.getForceConnected() && mLastConnectionLevel != ConnectionStatus.LEVEL_NOTCONNECTED) {
+ ProfileManager.setConntectedVpnProfileDisconnected(requireContext())
+ val service = mService;
+ if (service != null) {
+ try {
+ service.stopVPN(false)
+ } catch (e: RemoteException) {
+ VpnStatus.logException(e)
+ }
+ }
+ return
+ }
+
+ val intent = Intent(requireContext(), LaunchVPN::class.java)
+ intent.putExtra(LaunchVPN.EXTRA_KEY, alwaysOnVPN.uuidString)
+ intent.putExtra(OpenVPNService.EXTRA_START_REASON, "VPN started from homescreen.")
+ intent.action = Intent.ACTION_MAIN
+ startActivity(intent)
+ }
+
+ companion object {
+ private val MENU_IMPORT_PROFILE = Menu.FIRST + 1
+ private val MENU_IMPORT_AS = Menu.FIRST + 3
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/PackageAdapter.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/PackageAdapter.kt
index e2b8028d..3f7c2bf3 100644
--- a/main/src/ui/java/de/blinkt/openvpn/fragments/PackageAdapter.kt
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/PackageAdapter.kt
@@ -196,7 +196,7 @@ internal class PackageAdapter(c: Context, vp: VpnProfile) : RecyclerView.Adapter
private inner class ItemFilter : Filter() {
override fun performFiltering(constraint: CharSequence): FilterResults {
- val filterString = constraint.toString().toLowerCase(Locale.getDefault())
+ val filterString = constraint.toString().lowercase(Locale.getDefault())
val results = FilterResults()
@@ -212,10 +212,10 @@ internal class PackageAdapter(c: Context, vp: VpnProfile) : RecyclerView.Adapter
appName = pInfo.packageName
if (appName is String) {
- if (appName.toLowerCase(Locale.getDefault()).contains(filterString))
+ if (appName.lowercase(Locale.getDefault()).contains(filterString))
nlist.add(pInfo)
} else {
- if (appName.toString().toLowerCase(Locale.getDefault()).contains(filterString))
+ if (appName.toString().lowercase(Locale.getDefault()).contains(filterString))
nlist.add(pInfo)
}
}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.kt
index 944aa41a..30502582 100644
--- a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.kt
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.kt
@@ -102,6 +102,9 @@ class Settings_Authentication : OpenVpnPreferencesFragment(), Preference.OnPrefe
}
override fun saveSettings() {
+ if (!this::mExpectTLSCert.isInitialized) {
+ return;
+ }
mProfile.mExpectTLSCert = mExpectTLSCert.isChecked
mProfile.mCheckRemoteCN = mCheckRemoteCN.isChecked
mProfile.mRemoteCN = mRemoteCN.cnText
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.kt
index fef4861b..9839f83b 100644
--- a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.kt
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.kt
@@ -20,27 +20,20 @@ class Settings_IP : OpenVpnPreferencesFragment(), Preference.OnPreferenceChangeL
private lateinit var mDNS1: EditTextPreference
private lateinit var mDNS2: EditTextPreference
private lateinit var mNobind: CheckBoxPreference
- override fun onResume() {
- super.onResume()
- // Make sure default values are applied. In a real app, you would
- // want this in a shared function that is used to retrieve the
- // SharedPreferences wherever they are needed.
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_ipsettings)
+
PreferenceManager.setDefaultValues(
requireActivity(),
R.xml.vpn_ipsettings, false
)
- // Load the preferences from an XML resource
- addPreferencesFromResource(R.xml.vpn_ipsettings)
- }
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- /* Bind the preferences early to avoid loadingSetting which is called
- * from the superclass to access an uninitialised earlyinit property
- */
- super.onViewCreated(view, savedInstanceState)
}
override fun onBindPreferences() {
@@ -90,6 +83,10 @@ class Settings_IP : OpenVpnPreferencesFragment(), Preference.OnPreferenceChangeL
}
override fun saveSettings() {
+ // Since we maybe not have preferences bound yet, check if we actually have them bound.
+ if (!this::mUsePull.isInitialized) {
+ return;
+ }
mProfile.mUsePull = mUsePull.isChecked
mProfile.mIPv4Address = mIPv4.text
mProfile.mIPv6Address = mIPv6.text
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java
index 6b963bb8..99ba333e 100644
--- a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java
@@ -7,6 +7,7 @@ package de.blinkt.openvpn.fragments;
import android.os.Build;
import android.os.Bundle;
+import androidx.lifecycle.Lifecycle;
import androidx.preference.CheckBoxPreference;
import androidx.preference.EditTextPreference;
import androidx.preference.Preference;
@@ -53,6 +54,7 @@ public class Settings_Routing extends OpenVpnPreferencesFragment implements Pref
getPreferenceScreen().removePreference(mBlockUnusedAF);
loadSettings();
+
}
@Override
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java
deleted file mode 100644
index 28504268..00000000
--- a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java
+++ /dev/null
@@ -1,686 +0,0 @@
-/*
- * 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.Manifest;
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.content.pm.ShortcutInfo;
-import android.content.pm.ShortcutManager;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.PersistableBundle;
-
-import androidx.activity.result.ActivityResultCallback;
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.fragment.app.ListFragment;
-
-import android.text.Html;
-import android.text.Html.ImageGetter;
-import android.text.SpannableString;
-import android.text.SpannableStringBuilder;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.EditText;
-import android.widget.ImageButton;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.TreeSet;
-
-import de.blinkt.openvpn.LaunchVPN;
-import de.blinkt.openvpn.R;
-import de.blinkt.openvpn.VpnProfile;
-import de.blinkt.openvpn.activities.ConfigConverter;
-import de.blinkt.openvpn.activities.DisconnectVPN;
-import de.blinkt.openvpn.activities.FileSelect;
-import de.blinkt.openvpn.activities.VPNPreferences;
-import de.blinkt.openvpn.core.ConnectionStatus;
-import de.blinkt.openvpn.core.PasswordDialogFragment;
-import de.blinkt.openvpn.core.Preferences;
-import de.blinkt.openvpn.core.ProfileManager;
-import de.blinkt.openvpn.core.VpnStatus;
-
-import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT;
-import static de.blinkt.openvpn.core.OpenVPNService.DISCONNECT_VPN;
-import static de.blinkt.openvpn.core.OpenVPNService.EXTRA_CHALLENGE_TXT;
-import static de.blinkt.openvpn.core.OpenVPNService.EXTRA_START_REASON;
-
-
-public class VPNProfileList extends ListFragment implements OnClickListener, VpnStatus.StateListener {
-
- 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<VpnProfile> mArrayadapter;
- private Intent mLastIntent;
- private VpnProfile defaultVPN;
- private View mPermissionView;
- private ActivityResultLauncher<String> mPermReceiver;
-
- @Override
- public void updateState(String state, String logmessage, final int localizedResId, ConnectionStatus level, Intent intent) {
- requireActivity().runOnUiThread(() -> {
- mLastStatusMessage = VpnStatus.getLastCleanLogMessage(getActivity());
- mLastIntent = intent;
- mArrayadapter.notifyDataSetChanged();
- showUserRequestDialogIfNeeded(level, intent);
- });
- }
-
- private boolean showUserRequestDialogIfNeeded(ConnectionStatus level, Intent intent) {
- if (level == LEVEL_WAITING_FOR_USER_INPUT) {
- if (intent != null && intent.getStringExtra(EXTRA_CHALLENGE_TXT) != null) {
- PasswordDialogFragment pwInputFrag = PasswordDialogFragment.Companion.newInstance(intent, false);
-
- pwInputFrag.show(getParentFragmentManager(), "dialog");
- return true;
- }
- }
- return false;
- }
-
- @Override
- public void setConnectedVPN(String uuid) {
- }
-
- private void startOrStopVPN(VpnProfile profile) {
- if (VpnStatus.isVPNActive() && profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) {
- if (mLastIntent != null) {
- startActivity(mLastIntent);
- } else {
- Intent disconnectVPN = new Intent(getActivity(), DisconnectVPN.class);
- startActivity(disconnectVPN);
- }
- } else {
- startVPN(profile);
- }
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setHasOptionsMenu(true);
- setListAdapter();
-
- registerPermissionReceiver();
- }
-
- private void registerPermissionReceiver() {
- mPermReceiver = registerForActivityResult(new ActivityResultContracts.RequestPermission(),
- result -> checkForNotificationPermission(requireView()));
- }
-
- @RequiresApi(api = Build.VERSION_CODES.N_MR1)
- void updateDynamicShortcuts() {
- PersistableBundle versionExtras = new PersistableBundle();
- versionExtras.putInt("version", SHORTCUT_VERSION);
-
- ShortcutManager shortcutManager = getContext().getSystemService(ShortcutManager.class);
- if (shortcutManager.isRateLimitingActive())
- return;
-
- List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts();
- int maxvpn = shortcutManager.getMaxShortcutCountPerActivity() - 1;
-
-
- ShortcutInfo disconnectShortcut = new ShortcutInfo.Builder(getContext(), "disconnectVPN")
- .setShortLabel("Disconnect")
- .setLongLabel("Disconnect VPN")
- .setIntent(new Intent(getContext(), DisconnectVPN.class).setAction(DISCONNECT_VPN))
- .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_cancel))
- .setExtras(versionExtras)
- .build();
-
- LinkedList<ShortcutInfo> newShortcuts = new LinkedList<>();
- LinkedList<ShortcutInfo> updateShortcuts = new LinkedList<>();
-
- LinkedList<String> removeShortcuts = new LinkedList<>();
- LinkedList<String> disableShortcuts = new LinkedList<>();
-
- boolean addDisconnect = true;
-
-
- TreeSet<VpnProfile> sortedProfilesLRU = new TreeSet<VpnProfile>(new VpnProfileLRUComparator());
- ProfileManager profileManager = ProfileManager.getInstance(getContext());
- sortedProfilesLRU.addAll(profileManager.getProfiles());
-
- LinkedList<VpnProfile> LRUProfiles = new LinkedList<>();
- maxvpn = Math.min(maxvpn, sortedProfilesLRU.size());
-
- for (int i = 0; i < maxvpn; i++) {
- LRUProfiles.add(sortedProfilesLRU.pollFirst());
- }
-
- for (ShortcutInfo shortcut : shortcuts) {
- if (shortcut.getId().equals("disconnectVPN")) {
- addDisconnect = false;
- if (shortcut.getExtras() == null
- || shortcut.getExtras().getInt("version") != SHORTCUT_VERSION)
- updateShortcuts.add(disconnectShortcut);
-
- } else {
- VpnProfile p = ProfileManager.get(getContext(), shortcut.getId());
- if (p == null || p.profileDeleted) {
- if (shortcut.isEnabled()) {
- disableShortcuts.add(shortcut.getId());
- removeShortcuts.add(shortcut.getId());
- }
- if (!shortcut.isPinned())
- removeShortcuts.add(shortcut.getId());
- } else {
-
- if (LRUProfiles.contains(p))
- LRUProfiles.remove(p);
- else
- removeShortcuts.add(p.getUUIDString());
-
- if (!p.getName().equals(shortcut.getShortLabel())
- || shortcut.getExtras() == null
- || shortcut.getExtras().getInt("version") != SHORTCUT_VERSION)
- updateShortcuts.add(createShortcut(p));
-
-
- }
-
- }
-
- }
- if (addDisconnect)
- newShortcuts.add(disconnectShortcut);
- for (VpnProfile p : LRUProfiles)
- newShortcuts.add(createShortcut(p));
-
- if (updateShortcuts.size() > 0)
- shortcutManager.updateShortcuts(updateShortcuts);
- if (removeShortcuts.size() > 0)
- shortcutManager.removeDynamicShortcuts(removeShortcuts);
- if (newShortcuts.size() > 0)
- shortcutManager.addDynamicShortcuts(newShortcuts);
- if (disableShortcuts.size() > 0)
- shortcutManager.disableShortcuts(disableShortcuts, "VpnProfile does not exist anymore.");
- }
-
- @RequiresApi(Build.VERSION_CODES.N_MR1)
- ShortcutInfo createShortcut(VpnProfile profile) {
- Intent shortcutIntent = new Intent(Intent.ACTION_MAIN);
- shortcutIntent.setClass(requireContext(), LaunchVPN.class);
- shortcutIntent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString());
- shortcutIntent.setAction(Intent.ACTION_MAIN);
- shortcutIntent.putExtra(EXTRA_START_REASON, "shortcut");
- shortcutIntent.putExtra("EXTRA_HIDELOG", true);
-
- PersistableBundle versionExtras = new PersistableBundle();
- versionExtras.putInt("version", SHORTCUT_VERSION);
-
- return new ShortcutInfo.Builder(getContext(), profile.getUUIDString())
- .setShortLabel(profile.getName())
- .setLongLabel(getString(R.string.qs_connect, profile.getName()))
- .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_vpn_key))
- .setIntent(shortcutIntent)
- .setExtras(versionExtras)
- .build();
- }
-
- @Override
- public void onResume() {
- super.onResume();
- setListAdapter();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
- updateDynamicShortcuts();
- }
- VpnStatus.addStateListener(this);
- defaultVPN = ProfileManager.getAlwaysOnVPN(requireContext());
- }
-
- @Override
- public void onPause() {
- super.onPause();
- VpnStatus.removeStateListener(this);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View v = inflater.inflate(R.layout.vpn_profile_list, container, false);
-
- TextView newvpntext = (TextView) v.findViewById(R.id.add_new_vpn_hint);
- TextView importvpntext = (TextView) v.findViewById(R.id.import_vpn_hint);
-
- newvpntext.setText(Html.fromHtml(getString(R.string.add_new_vpn_hint), new MiniImageGetter(), null));
- importvpntext.setText(Html.fromHtml(getString(R.string.vpn_import_hint), new MiniImageGetter(), null));
-
- ImageButton fab_add = (ImageButton) v.findViewById(R.id.fab_add);
- ImageButton fab_import = (ImageButton) v.findViewById(R.id.fab_import);
- if (fab_add != null)
- fab_add.setOnClickListener(this);
-
- if (fab_import != null)
- fab_import.setOnClickListener(this);
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
- checkForNotificationPermission(v);
-
-
- return v;
-
- }
-
- private void checkForNotificationPermission(View v) {
- mPermissionView = v.findViewById(R.id.notification_permission);
- boolean permissionGranted = (requireActivity().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED);
- mPermissionView.setVisibility(permissionGranted ? View.GONE : View.VISIBLE);
-
- mPermissionView.setOnClickListener((view) -> {
- mPermReceiver.launch(Manifest.permission.POST_NOTIFICATIONS);
- });
- }
-
- private void setListAdapter() {
- if (mArrayadapter == null) {
- mArrayadapter = new VPNArrayAdapter(getActivity(), R.layout.vpn_list_item, R.id.vpn_item_title);
-
- }
- populateVpnList();
- }
-
- private void populateVpnList() {
- boolean sortByLRU = Preferences.getDefaultSharedPreferences(requireActivity()).getBoolean(PREF_SORT_BY_LRU, false);
- getPM().refreshVPNList(requireContext());
- Collection<VpnProfile> allvpn = getPM().getProfiles();
- TreeSet<VpnProfile> sortedset;
- if (sortByLRU)
- sortedset = new TreeSet<>(new VpnProfileLRUComparator());
- else
- sortedset = new TreeSet<>(new VpnProfileNameComparator());
-
- sortedset.addAll(allvpn);
- mArrayadapter.clear();
- mArrayadapter.addAll(sortedset);
-
- setListAdapter(mArrayadapter);
- mArrayadapter.notifyDataSetChanged();
- }
-
- @Override
- public void onCreateOptionsMenu(Menu menu, @NonNull MenuInflater inflater) {
- menu.add(0, MENU_ADD_PROFILE, 0, R.string.menu_add_profile)
- .setIcon(R.drawable.ic_menu_add)
- .setAlphabeticShortcut('a')
- .setTitleCondensed(getActivity().getString(R.string.add))
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
-
- menu.add(0, MENU_IMPORT_PROFILE, 0, R.string.menu_import)
- .setIcon(R.drawable.ic_menu_import)
- .setAlphabeticShortcut('i')
- .setTitleCondensed(getActivity().getString(R.string.menu_import_short))
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
-
- menu.add(0, MENU_CHANGE_SORTING, 0, R.string.change_sorting)
- .setIcon(R.drawable.ic_sort)
- .setAlphabeticShortcut('s')
- .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) {
- final int itemId = item.getItemId();
- if (itemId == MENU_ADD_PROFILE) {
- onAddOrDuplicateProfile(null);
- return true;
- } else if (itemId == MENU_IMPORT_PROFILE) {
- 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() {
- ImportRemoteConfig asImportFrag = ImportRemoteConfig.newInstance(null);
- asImportFrag.show(getParentFragmentManager(), "dialog");
- return true;
- }
-
- private boolean changeSorting() {
- SharedPreferences prefs = Preferences.getDefaultSharedPreferences(requireActivity());
- boolean oldValue = prefs.getBoolean(PREF_SORT_BY_LRU, false);
- SharedPreferences.Editor prefsedit = prefs.edit();
- if (oldValue) {
- Toast.makeText(getActivity(), R.string.sorted_az, Toast.LENGTH_SHORT).show();
- prefsedit.putBoolean(PREF_SORT_BY_LRU, false);
- } else {
- prefsedit.putBoolean(PREF_SORT_BY_LRU, true);
- Toast.makeText(getActivity(), R.string.sorted_lru, Toast.LENGTH_SHORT).show();
- }
- prefsedit.apply();
- populateVpnList();
- return true;
- }
-
- @Override
- public void onClick(View v) {
- switch (v.getId()) {
- case R.id.fab_import:
- startImportConfigFilePicker();
- break;
- case R.id.fab_add:
- onAddOrDuplicateProfile(null);
- break;
- }
- }
-
- private boolean startImportConfigFilePicker() {
- boolean startOldFileDialog = true;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !Utils.alwaysUseOldFileChooser(getActivity()))
- startOldFileDialog = !startFilePicker();
-
- if (startOldFileDialog)
- startImportConfig();
-
- return true;
- }
-
- @TargetApi(Build.VERSION_CODES.KITKAT)
- private boolean startFilePicker() {
-
- Intent i = Utils.getFilePickerIntent(getActivity(), Utils.FileType.OVPN_CONFIG);
- if (i != null) {
- startActivityForResult(i, FILE_PICKER_RESULT_KITKAT);
- return true;
- } else
- return false;
- }
-
- private void startImportConfig() {
- Intent intent = new Intent(getActivity(), FileSelect.class);
- intent.putExtra(FileSelect.NO_INLINE_SELECTION, true);
- intent.putExtra(FileSelect.WINDOW_TITLE, R.string.import_configuration_file);
- startActivityForResult(intent, SELECT_PROFILE);
- }
-
- private void onAddOrDuplicateProfile(final VpnProfile mCopyProfile) {
- Context context = getActivity();
- if (context != null) {
- final EditText entry = new EditText(context);
- entry.setSingleLine();
- entry.setContentDescription(getString(R.string.name_of_the_vpn_profile));
-
- AlertDialog.Builder dialog = new AlertDialog.Builder(context);
- if (mCopyProfile == null)
- dialog.setTitle(R.string.menu_add_profile);
- else {
- dialog.setTitle(context.getString(R.string.duplicate_profile_title, mCopyProfile.mName));
- entry.setText(getString(R.string.copy_of_profile, mCopyProfile.mName));
- }
-
- dialog.setMessage(R.string.add_profile_name_prompt);
- dialog.setView(entry);
-
- dialog.setNeutralButton(R.string.menu_import_short,
- (dialog1, which) -> startImportConfigFilePicker());
- dialog.setPositiveButton(android.R.string.ok,
- (dialog12, which) -> {
- String name = entry.getText().toString();
- if (getPM().getProfileByName(name) == null) {
- VpnProfile profile;
- if (mCopyProfile != null) {
- profile = mCopyProfile.copy(name);
- // Remove restrictions on copy profile
- profile.mProfileCreator = null;
- profile.mUserEditable = true;
- } else
- profile = new VpnProfile(name);
-
- addProfile(profile);
- editVPN(profile);
- } else {
- Toast.makeText(getActivity(), R.string.duplicate_profile_name, Toast.LENGTH_LONG).show();
- }
- });
- dialog.setNegativeButton(android.R.string.cancel, null);
- dialog.create().show();
- }
-
- }
-
- private void addProfile(VpnProfile profile) {
- getPM().addProfile(profile);
- getPM().saveProfileList(getActivity());
- getPM().saveProfile(getActivity(), profile);
- mArrayadapter.add(profile);
- }
-
- private ProfileManager getPM() {
- return ProfileManager.getInstance(getActivity());
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
-
- if (resultCode == RESULT_VPN_DELETED) {
- if (mArrayadapter != null && mEditProfile != null)
- mArrayadapter.remove(mEditProfile);
- } else if (resultCode == RESULT_VPN_DUPLICATE && data != null) {
- String profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
- VpnProfile profile = ProfileManager.get(getActivity(), profileUUID);
- if (profile != null)
- onAddOrDuplicateProfile(profile);
- }
-
- if (resultCode != Activity.RESULT_OK)
- return;
-
- if (requestCode == START_VPN_CONFIG) {
- String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
-
- VpnProfile profile = ProfileManager.get(getActivity(), configuredVPN);
- getPM().saveProfile(getActivity(), profile);
- // Name could be modified, reset List adapter
- setListAdapter();
-
- } else if (requestCode == SELECT_PROFILE) {
- String fileData = data.getStringExtra(FileSelect.RESULT_DATA);
- Uri uri = new Uri.Builder().path(fileData).scheme("file").build();
-
- startConfigImport(uri);
- } else if (requestCode == IMPORT_PROFILE) {
- String profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
- mArrayadapter.add(ProfileManager.get(getActivity(), profileUUID));
- } else if (requestCode == FILE_PICKER_RESULT_KITKAT) {
- if (data != null) {
- Uri uri = data.getData();
- startConfigImport(uri);
- }
- }
-
- }
-
- private void startConfigImport(Uri uri) {
- Intent startImport = new Intent(getActivity(), ConfigConverter.class);
- startImport.setAction(ConfigConverter.IMPORT_PROFILE);
- startImport.setData(uri);
- startActivityForResult(startImport, IMPORT_PROFILE);
- }
-
- private void editVPN(VpnProfile profile) {
- mEditProfile = profile;
- Intent vprefintent = new Intent(getActivity(), VPNPreferences.class)
- .putExtra(getActivity().getPackageName() + ".profileUUID", profile.getUUID().toString());
-
- startActivityForResult(vprefintent, START_VPN_CONFIG);
- }
-
- private void startVPN(VpnProfile profile) {
-
- getPM().saveProfile(getActivity(), profile);
-
- Intent intent = new Intent(getActivity(), LaunchVPN.class);
- intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString());
- intent.putExtra(EXTRA_START_REASON, "main profile list");
- intent.setAction(Intent.ACTION_MAIN);
- startActivity(intent);
- }
-
- static class VpnProfileNameComparator implements Comparator<VpnProfile> {
-
- @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<VpnProfile> {
-
- 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<VpnProfile> {
-
- public VPNArrayAdapter(Context context, int resource,
- int textViewResourceId) {
- super(context, resource, textViewResourceId);
- }
-
- @NonNull
- @Override
- public View getView(final int position, View convertView, @NonNull 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(v1 -> startOrStopVPN(profile));
-
- View settingsview = v.findViewById(R.id.quickedit_settings);
- settingsview.setOnClickListener(view -> editVPN(profile));
-
- TextView subtitle = v.findViewById(R.id.vpn_item_subtitle);
- SpannableStringBuilder warningText = Utils.getWarningText(requireContext(), profile);
-
- if (profile == defaultVPN) {
- if (warningText.length() > 0)
- warningText.append(" ");
- warningText.append(new SpannableString("Default VPN"));
- }
-
- if (profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) {
- subtitle.setText(mLastStatusMessage);
- subtitle.setVisibility(View.VISIBLE);
- } else {
- subtitle.setText(warningText);
- if (warningText.length() > 0)
- subtitle.setVisibility(View.VISIBLE);
- else
- 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 = requireActivity().getResources().getDrawable(R.drawable.ic_menu_add_grey, requireActivity().getTheme());
- else if ("ic_menu_archive".equals(source))
- d = requireActivity().getResources().getDrawable(R.drawable.ic_menu_import_grey, requireActivity().getTheme());
-
-
- if (d != null) {
- d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
- return d;
- } else {
- return null;
- }
- }
- }
-}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.kt
new file mode 100644
index 00000000..263d36b5
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.kt
@@ -0,0 +1,654 @@
+/*
+ * 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.Manifest
+import android.annotation.TargetApi
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.PersistableBundle
+import android.text.Html
+import android.text.SpannableString
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.EditText
+import android.widget.ImageButton
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.result.ActivityResultCallback
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
+import androidx.annotation.RequiresApi
+import androidx.fragment.app.ListFragment
+import de.blinkt.openvpn.LaunchVPN
+import de.blinkt.openvpn.R
+import de.blinkt.openvpn.VpnProfile
+import de.blinkt.openvpn.activities.BaseActivity
+import de.blinkt.openvpn.activities.ConfigConverter
+import de.blinkt.openvpn.activities.DisconnectVPN
+import de.blinkt.openvpn.activities.FileSelect
+import de.blinkt.openvpn.activities.VPNPreferences
+import de.blinkt.openvpn.core.ConnectionStatus
+import de.blinkt.openvpn.core.OpenVPNService
+import de.blinkt.openvpn.core.PasswordDialogFragment.Companion.newInstance
+import de.blinkt.openvpn.core.Preferences
+import de.blinkt.openvpn.core.ProfileManager
+import de.blinkt.openvpn.core.VpnStatus
+import de.blinkt.openvpn.core.VpnStatus.StateListener
+import de.blinkt.openvpn.fragments.ImportRemoteConfig.Companion.newInstance
+import de.blinkt.openvpn.fragments.Utils.alwaysUseOldFileChooser
+import de.blinkt.openvpn.fragments.Utils.getWarningText
+import java.util.LinkedList
+import java.util.TreeSet
+import kotlin.math.min
+
+class VPNProfileList : ListFragment(), View.OnClickListener, StateListener {
+ protected var mEditProfile: VpnProfile? = null
+ private var mLastStatusMessage: String? = null
+ private var mArrayadapter: ArrayAdapter<VpnProfile>? = null
+ private var mLastIntent: Intent? = null
+ private var defaultVPN: VpnProfile? = null
+ private lateinit var mPermissionView: View
+ private lateinit var mPermReceiver: ActivityResultLauncher<String>
+
+ override fun updateState(
+ state: String?,
+ logmessage: String?,
+ localizedResId: Int,
+ level: ConnectionStatus?,
+ intent: Intent?
+ ) {
+ requireActivity().runOnUiThread(Runnable {
+ mLastStatusMessage = VpnStatus.getLastCleanLogMessage(getActivity())
+ mLastIntent = intent
+ mArrayadapter!!.notifyDataSetChanged()
+ showUserRequestDialogIfNeeded(level, intent)
+ })
+ }
+
+ private fun showUserRequestDialogIfNeeded(level: ConnectionStatus?, intent: Intent?): Boolean {
+ if (level == ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT) {
+ if (intent != null && intent.getStringExtra(OpenVPNService.EXTRA_CHALLENGE_TXT) != null) {
+ val pwInputFrag = newInstance(intent, false)
+
+ pwInputFrag!!.show(getParentFragmentManager(), "dialog")
+ return true
+ }
+ }
+ return false
+ }
+
+ override fun setConnectedVPN(uuid: String?) {
+ }
+
+ private fun startOrStopVPN(profile: VpnProfile) {
+ if (VpnStatus.isVPNActive() && profile.getUUIDString() == VpnStatus.getLastConnectedVPNProfile()) {
+ if (mLastIntent != null) {
+ startActivity(mLastIntent!!)
+ } else {
+ val disconnectVPN = Intent(getActivity(), DisconnectVPN::class.java)
+ startActivity(disconnectVPN)
+ }
+ } else {
+ startVPN(profile)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
+ setListAdapter()
+
+ registerPermissionReceiver()
+ }
+
+ private fun registerPermissionReceiver() {
+ mPermReceiver = registerForActivityResult<String, Boolean>(
+ RequestPermission(),
+ ActivityResultCallback { result: Boolean? -> checkForNotificationPermission(requireView()) })
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N_MR1)
+ fun updateDynamicShortcuts() {
+ val versionExtras = PersistableBundle()
+ versionExtras.putInt("version", SHORTCUT_VERSION)
+
+ val shortcutManager =
+ getContext()!!.getSystemService<ShortcutManager>(ShortcutManager::class.java)
+ if (shortcutManager.isRateLimitingActive()) return
+
+ val shortcuts = shortcutManager.getDynamicShortcuts()
+ var maxvpn = shortcutManager.getMaxShortcutCountPerActivity() - 1
+
+
+ val disconnectShortcut = ShortcutInfo.Builder(getContext(), "disconnectVPN")
+ .setShortLabel("Disconnect")
+ .setLongLabel("Disconnect VPN")
+ .setIntent(
+ Intent(
+ getContext(),
+ DisconnectVPN::class.java
+ ).setAction(OpenVPNService.DISCONNECT_VPN)
+ )
+ .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_cancel))
+ .setExtras(versionExtras)
+ .build()
+
+ val newShortcuts = LinkedList<ShortcutInfo>()
+ val updateShortcuts = LinkedList<ShortcutInfo>()
+
+ val removeShortcuts = LinkedList<String>()
+ val disableShortcuts = LinkedList<String>()
+
+ var addDisconnect = true
+
+
+ val sortedProfilesLRU = TreeSet<VpnProfile?>(VpnProfileLRUComparator())
+ val profileManager = ProfileManager.getInstance(getContext())
+ sortedProfilesLRU.addAll(profileManager.getProfiles())
+
+ val LRUProfiles = LinkedList<VpnProfile>()
+ maxvpn = min(maxvpn, sortedProfilesLRU.size)
+
+ for (i in 0..<maxvpn) {
+ LRUProfiles.add(sortedProfilesLRU.pollFirst()!!)
+ }
+
+ for (shortcut in shortcuts) {
+ if (shortcut.getId() == "disconnectVPN") {
+ addDisconnect = false
+ if (shortcut.getExtras() == null
+ || shortcut.getExtras()!!.getInt("version") != SHORTCUT_VERSION
+ ) updateShortcuts.add(disconnectShortcut)
+ } else {
+ val p = ProfileManager.get(getContext(), shortcut.getId())
+ if (p == null || p.profileDeleted) {
+ if (shortcut.isEnabled()) {
+ disableShortcuts.add(shortcut.getId())
+ removeShortcuts.add(shortcut.getId())
+ }
+ if (!shortcut.isPinned()) removeShortcuts.add(shortcut.getId())
+ } else {
+ if (LRUProfiles.contains(p)) LRUProfiles.remove(p)
+ else removeShortcuts.add(p.getUUIDString())
+
+ if ((p.getName() != shortcut.getShortLabel()) || shortcut.getExtras() == null || shortcut.getExtras()!!
+ .getInt("version") != SHORTCUT_VERSION
+ ) updateShortcuts.add(createShortcut(p))
+ }
+ }
+ }
+ if (addDisconnect) newShortcuts.add(disconnectShortcut)
+ for (p in LRUProfiles) newShortcuts.add(createShortcut(p))
+
+ if (updateShortcuts.size > 0) shortcutManager.updateShortcuts(updateShortcuts)
+ if (removeShortcuts.size > 0) shortcutManager.removeDynamicShortcuts(removeShortcuts)
+ if (newShortcuts.size > 0) shortcutManager.addDynamicShortcuts(newShortcuts)
+ if (disableShortcuts.size > 0) shortcutManager.disableShortcuts(
+ disableShortcuts,
+ "VpnProfile does not exist anymore."
+ )
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N_MR1)
+ fun createShortcut(profile: VpnProfile): ShortcutInfo {
+ val shortcutIntent = Intent(Intent.ACTION_MAIN)
+ shortcutIntent.setClass(requireContext(), LaunchVPN::class.java)
+ shortcutIntent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString())
+ shortcutIntent.setAction(Intent.ACTION_MAIN)
+ shortcutIntent.putExtra(OpenVPNService.EXTRA_START_REASON, "shortcut")
+ shortcutIntent.putExtra("EXTRA_HIDELOG", true)
+
+ val versionExtras = PersistableBundle()
+ versionExtras.putInt("version", SHORTCUT_VERSION)
+
+ return ShortcutInfo.Builder(getContext(), profile.getUUIDString())
+ .setShortLabel(profile.getName())
+ .setLongLabel(getString(R.string.qs_connect, profile.getName()))
+ .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_vpn_key))
+ .setIntent(shortcutIntent)
+ .setExtras(versionExtras)
+ .build()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ setListAdapter()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
+ updateDynamicShortcuts()
+ }
+ VpnStatus.addStateListener(this)
+ defaultVPN = ProfileManager.getAlwaysOnVPN(requireContext())
+ }
+
+ override fun onPause() {
+ super.onPause()
+ VpnStatus.removeStateListener(this)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val v = inflater.inflate(R.layout.vpn_profile_list, container, false)
+
+ val newvpntext = v.findViewById<View?>(R.id.add_new_vpn_hint) as TextView
+ val importvpntext = v.findViewById<View?>(R.id.import_vpn_hint) as TextView
+
+ newvpntext.setText(
+ Html.fromHtml(
+ getString(R.string.add_new_vpn_hint),
+ MiniImageGetter(),
+ null
+ )
+ )
+ importvpntext.setText(
+ Html.fromHtml(
+ getString(R.string.vpn_import_hint),
+ MiniImageGetter(),
+ null
+ )
+ )
+
+ val fab_add = v.findViewById<View?>(R.id.fab_add) as ImageButton?
+ val fab_import = v.findViewById<View?>(R.id.fab_import) as ImageButton?
+ if (fab_add != null) fab_add.setOnClickListener(this)
+
+ if (fab_import != null) fab_import.setOnClickListener(this)
+
+ // TV builds show the minimal UI that already have the notification
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !((activity as BaseActivity).isAndroidTV))
+ checkForNotificationPermission(v)
+
+ return v
+ }
+
+ private fun checkForNotificationPermission(v: View) {
+ mPermissionView = v.findViewById<View>(R.id.notification_permission)
+ val permissionGranted =
+ (requireActivity().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED)
+ mPermissionView.setVisibility(if (permissionGranted) View.GONE else View.VISIBLE)
+
+ mPermissionView.setOnClickListener(View.OnClickListener { view: View? ->
+ mPermReceiver.launch(
+ Manifest.permission.POST_NOTIFICATIONS
+ )
+ })
+ }
+
+ private fun setListAdapter() {
+ if (mArrayadapter == null) {
+ mArrayadapter =
+ VPNArrayAdapter(getActivity()!!, R.layout.vpn_list_item, R.id.vpn_item_title)
+ }
+ populateVpnList()
+ }
+
+ private fun populateVpnList() {
+ val sortByLRU = Preferences.getDefaultSharedPreferences(requireActivity()).getBoolean(
+ PREF_SORT_BY_LRU, false
+ )
+ this.pM.refreshVPNList(requireContext())
+ val allvpn: MutableCollection<VpnProfile?>? = this.pM.getProfiles()
+ val sortedset: TreeSet<VpnProfile?>?
+ if (sortByLRU) sortedset = TreeSet<VpnProfile?>(VpnProfileLRUComparator())
+ else sortedset = TreeSet<VpnProfile?>(VpnProfileNameComparator())
+
+ sortedset.addAll(allvpn!!)
+ mArrayadapter!!.clear()
+ mArrayadapter!!.addAll(sortedset)
+
+ setListAdapter(mArrayadapter)
+ mArrayadapter!!.notifyDataSetChanged()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ menu.add(0, MENU_ADD_PROFILE, 0, R.string.menu_add_profile)
+ .setIcon(R.drawable.ic_menu_add)
+ .setAlphabeticShortcut('a')
+ .setTitleCondensed(getActivity()!!.getString(R.string.add))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
+
+ menu.add(0, MENU_IMPORT_PROFILE, 0, R.string.menu_import)
+ .setIcon(R.drawable.ic_menu_import)
+ .setAlphabeticShortcut('i')
+ .setTitleCondensed(getActivity()!!.getString(R.string.menu_import_short))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
+
+ menu.add(0, MENU_CHANGE_SORTING, 0, R.string.change_sorting)
+ .setIcon(R.drawable.ic_sort)
+ .setAlphabeticShortcut('s')
+ .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_download)
+ .setAlphabeticShortcut('p')
+ .setTitleCondensed("Import AS")
+ .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ val itemId = item.getItemId()
+ if (itemId == MENU_ADD_PROFILE) {
+ onAddOrDuplicateProfile(null)
+ return true
+ } else if (itemId == MENU_IMPORT_PROFILE) {
+ return startImportConfigFilePicker()
+ } else if (itemId == MENU_CHANGE_SORTING) {
+ return changeSorting()
+ } else if (itemId == MENU_IMPORT_AS) {
+ return startASProfileImport()
+ } else {
+ return super.onOptionsItemSelected(item)
+ }
+ }
+
+ private fun startASProfileImport(): Boolean {
+ val asImportFrag = newInstance(null)
+ asImportFrag.show(getParentFragmentManager(), "dialog")
+ return true
+ }
+
+ private fun changeSorting(): Boolean {
+ val prefs = Preferences.getDefaultSharedPreferences(requireActivity())
+ val oldValue = prefs.getBoolean(PREF_SORT_BY_LRU, false)
+ val prefsedit = prefs.edit()
+ if (oldValue) {
+ Toast.makeText(getActivity(), R.string.sorted_az, Toast.LENGTH_SHORT).show()
+ prefsedit.putBoolean(PREF_SORT_BY_LRU, false)
+ } else {
+ prefsedit.putBoolean(PREF_SORT_BY_LRU, true)
+ Toast.makeText(getActivity(), R.string.sorted_lru, Toast.LENGTH_SHORT).show()
+ }
+ prefsedit.apply()
+ populateVpnList()
+ return true
+ }
+
+ override fun onClick(v: View) {
+ when (v.getId()) {
+ R.id.fab_import -> startImportConfigFilePicker()
+ R.id.fab_add -> onAddOrDuplicateProfile(null)
+ }
+ }
+
+ private fun startImportConfigFilePicker(): Boolean {
+ var startOldFileDialog = true
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !alwaysUseOldFileChooser(
+ getActivity()
+ )
+ ) startOldFileDialog = !startFilePicker()
+
+ if (startOldFileDialog) startImportConfig()
+
+ return true
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private fun startFilePicker(): Boolean {
+ val i = Utils.getFilePickerIntent(getActivity()!!, Utils.FileType.OVPN_CONFIG)
+ if (i != null) {
+ startActivityForResult(i, FILE_PICKER_RESULT_KITKAT)
+ return true
+ } else return false
+ }
+
+ private fun startImportConfig() {
+ val intent = Intent(getActivity(), FileSelect::class.java)
+ intent.putExtra(FileSelect.NO_INLINE_SELECTION, true)
+ intent.putExtra(FileSelect.WINDOW_TITLE, R.string.import_configuration_file)
+ startActivityForResult(intent, SELECT_PROFILE)
+ }
+
+ private fun onAddOrDuplicateProfile(mCopyProfile: VpnProfile?) {
+ val context: Context? = getActivity()
+ if (context != null) {
+ val entry = EditText(context)
+ entry.setSingleLine()
+ entry.setContentDescription(getString(R.string.name_of_the_vpn_profile))
+
+ val dialog = AlertDialog.Builder(context)
+ if (mCopyProfile == null) dialog.setTitle(R.string.menu_add_profile)
+ else {
+ dialog.setTitle(
+ context.getString(
+ R.string.duplicate_profile_title,
+ mCopyProfile.mName
+ )
+ )
+ entry.setText(getString(R.string.copy_of_profile, mCopyProfile.mName))
+ }
+
+ dialog.setMessage(R.string.add_profile_name_prompt)
+ dialog.setView(entry)
+
+ dialog.setNeutralButton(
+ R.string.menu_import_short
+ ) { dialog1: DialogInterface?, which: Int -> startImportConfigFilePicker() }
+ dialog.setPositiveButton(
+ android.R.string.ok
+ ) { dialog12: DialogInterface?, which: Int ->
+ val name = entry.getText().toString()
+ if (this.pM.getProfileByName(name) == null) {
+ val profile: VpnProfile
+ if (mCopyProfile != null) {
+ profile = mCopyProfile.copy(name)
+ // Remove restrictions on copy profile
+ profile.mProfileCreator = null
+ profile.mUserEditable = true
+ } else profile = VpnProfile(name)
+
+ addProfile(profile)
+ editVPN(profile)
+ } else {
+ Toast.makeText(
+ getActivity(),
+ R.string.duplicate_profile_name,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ dialog.setNegativeButton(android.R.string.cancel, null)
+ dialog.create().show()
+ }
+ }
+
+ private fun addProfile(profile: VpnProfile) {
+ this.pM.addProfile(profile)
+ this.pM.saveProfileList(getActivity())
+ profile.addChangeLogEntry("empty profile added via main profile list")
+ ProfileManager.saveProfile(getActivity(), profile)
+ mArrayadapter!!.add(profile)
+ }
+
+ private val pM: ProfileManager
+ get() = ProfileManager.getInstance(getActivity())
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+
+ if (resultCode == RESULT_VPN_DELETED) {
+ if (mArrayadapter != null && mEditProfile != null) mArrayadapter!!.remove(mEditProfile)
+ } else if (resultCode == RESULT_VPN_DUPLICATE && data != null) {
+ val profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID)
+ val profile = ProfileManager.get(getActivity(), profileUUID)
+ if (profile != null) onAddOrDuplicateProfile(profile)
+ }
+
+ if (resultCode != Activity.RESULT_OK) return
+
+ if (requestCode == EDIT_VPN_CONFIG) {
+ val configuredVPN = data!!.getStringExtra(VpnProfile.EXTRA_PROFILEUUID)
+
+ val profile = ProfileManager.get(getActivity(), configuredVPN)
+ profile.addChangeLogEntry("Profile edited by user")
+ ProfileManager.saveProfile(getActivity(), profile)
+ // Name could be modified, reset List adapter
+ setListAdapter()
+ } else if (requestCode == SELECT_PROFILE) {
+ val fileData = data!!.getStringExtra(FileSelect.RESULT_DATA)
+ val uri = Uri.Builder().path(fileData).scheme("file").build()
+
+ startConfigImport(uri)
+ } else if (requestCode == IMPORT_PROFILE) {
+ val profileUUID = data!!.getStringExtra(VpnProfile.EXTRA_PROFILEUUID)
+ mArrayadapter!!.add(ProfileManager.get(getActivity(), profileUUID))
+ } else if (requestCode == FILE_PICKER_RESULT_KITKAT) {
+ if (data != null) {
+ val uri = data.getData()
+ startConfigImport(uri)
+ }
+ }
+ }
+
+ private fun startConfigImport(uri: Uri?) {
+ val startImport = Intent(getActivity(), ConfigConverter::class.java)
+ startImport.setAction(ConfigConverter.IMPORT_PROFILE)
+ startImport.setData(uri)
+ startActivityForResult(startImport, IMPORT_PROFILE)
+ }
+
+ private fun editVPN(profile: VpnProfile) {
+ mEditProfile = profile
+ val vprefintent = Intent(getActivity(), VPNPreferences::class.java)
+ .putExtra(
+ getActivity()!!.getPackageName() + ".profileUUID",
+ profile.getUUID().toString()
+ )
+
+ startActivityForResult(vprefintent, EDIT_VPN_CONFIG)
+ }
+
+ private fun startVPN(profile: VpnProfile) {
+ ProfileManager.saveProfile(getActivity(), profile)
+
+ val intent = Intent(getActivity(), LaunchVPN::class.java)
+ intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString())
+ intent.putExtra(OpenVPNService.EXTRA_START_REASON, "main profile list")
+ intent.setAction(Intent.ACTION_MAIN)
+ startActivity(intent)
+ }
+
+ internal class VpnProfileNameComparator : Comparator<VpnProfile?> {
+ override fun compare(lhs: VpnProfile?, rhs: VpnProfile?): Int {
+ 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)
+ }
+ }
+
+ internal class VpnProfileLRUComparator : Comparator<VpnProfile?> {
+ var nameComparator: VpnProfileNameComparator = VpnProfileNameComparator()
+
+ override fun compare(lhs: VpnProfile?, rhs: VpnProfile?): Int {
+ 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 inner class VPNArrayAdapter(
+ context: Context, resource: Int,
+ textViewResourceId: Int
+ ) : ArrayAdapter<VpnProfile>(context, resource, textViewResourceId) {
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val v = super.getView(position, convertView, parent)
+
+ val profile = getListAdapter()!!.getItem(position) as VpnProfile
+
+ val titleview = v.findViewById<View>(R.id.vpn_list_item_left)
+ titleview.setOnClickListener(View.OnClickListener { v1: View? -> startOrStopVPN(profile) })
+
+ val settingsview = v.findViewById<View>(R.id.quickedit_settings)
+ settingsview.setOnClickListener(View.OnClickListener { view: View? -> editVPN(profile) })
+
+ val subtitle = v.findViewById<TextView>(R.id.vpn_item_subtitle)
+ val warningText = getWarningText(requireContext(), profile)
+
+ if (profile === defaultVPN) {
+ if (warningText.length > 0) warningText.append(" ")
+ warningText.append(SpannableString("Default VPN"))
+ }
+
+ if (profile.getUUIDString() == VpnStatus.getLastConnectedVPNProfile()) {
+ subtitle.setText(mLastStatusMessage)
+ subtitle.setVisibility(View.VISIBLE)
+ } else {
+ subtitle.setText(warningText)
+ if (warningText.length > 0) subtitle.setVisibility(View.VISIBLE)
+ else subtitle.setVisibility(View.GONE)
+ }
+
+
+ return v
+ }
+ }
+
+ internal inner class MiniImageGetter : Html.ImageGetter {
+ override fun getDrawable(source: String?): Drawable? {
+ var d: Drawable? = null
+ if ("ic_menu_add" == source) d = requireActivity().getResources()
+ .getDrawable(R.drawable.ic_menu_add_grey, requireActivity().getTheme())
+ else if ("ic_menu_archive" == source) d = requireActivity().getResources()
+ .getDrawable(R.drawable.ic_menu_import_grey, requireActivity().getTheme())
+
+
+ if (d != null) {
+ d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight())
+ return d
+ } else {
+ return null
+ }
+ }
+ }
+
+ companion object {
+ val RESULT_VPN_DELETED: Int = Activity.RESULT_FIRST_USER
+ val RESULT_VPN_DUPLICATE: Int = Activity.RESULT_FIRST_USER + 1
+
+ // Shortcut version is increased to refresh all shortcuts
+ const val SHORTCUT_VERSION: Int = 1
+ private val MENU_ADD_PROFILE = Menu.FIRST
+ private const val EDIT_VPN_CONFIG = 92
+ private const val SELECT_PROFILE = 43
+ private const val IMPORT_PROFILE = 231
+ private const val FILE_PICKER_RESULT_KITKAT = 392
+ private val MENU_IMPORT_PROFILE = Menu.FIRST + 1
+ private val MENU_CHANGE_SORTING = Menu.FIRST + 2
+ private val MENU_IMPORT_AS = Menu.FIRST + 3
+ private const val PREF_SORT_BY_LRU = "sortProfilesByLRU"
+ }
+}