diff options
| author | cyberta <cyberta@riseup.net> | 2026-03-09 23:59:50 +0100 |
|---|---|---|
| committer | cyberta <cyberta@riseup.net> | 2026-03-09 23:59:50 +0100 |
| commit | 733d1ae5bbb5356a228e42a013660743db1c7073 (patch) | |
| tree | 550f2285ef072e7dcc482d648fccc09222a35791 /main/src/ui/java | |
| parent | b1c21e7e1fbc0d09e3d121b89651482a0bb02efd (diff) | |
| parent | 73ce3c99dde39207990d66c21be8201228cd1f09 (diff) | |
Merge branch 'schwabe_master' into ssh_new_master
Diffstat (limited to 'main/src/ui/java')
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" + } +} |
