@@ -3,7 +3,6 @@ package org.torproject.android.ui.connect
33import android.app.AlarmManager
44import android.content.Context
55import android.content.Intent
6- import android.net.VpnService
76import android.os.Build
87import android.os.Bundle
98import android.view.LayoutInflater
@@ -26,12 +25,12 @@ import kotlinx.coroutines.launch
2625import net.freehaven.tor.control.TorControlCommands
2726import org.torproject.android.OrbotActivity
2827import org.torproject.android.R
29- import org.torproject.android.util.putNotSystem
3028import org.torproject.android.util.sendIntentToService
3129import org.torproject.android.databinding.FragmentConnectBinding
3230import org.torproject.android.service.OrbotConstants
3331import org.torproject.android.service.OrbotService
3432import org.torproject.android.service.circumvention.Transport
33+ import org.torproject.android.service.vpn.VpnServicePrepareWrapper
3534import org.torproject.android.util.Prefs
3635import org.torproject.android.ui.OrbotMenuAction
3736import org.torproject.jni.TorService
@@ -48,10 +47,21 @@ class ConnectFragment : Fragment(),
4847 private val lastStatus: String
4948 get() = (activity as ? OrbotActivity )?.previousReceivedTorStatus ? : " "
5049
51- private val startTorResultLauncher =
50+ private val startTorVpnResultLauncher =
5251 registerForActivityResult(ActivityResultContracts .StartActivityForResult ()) { result ->
52+ // The user pressed OK, we can start Tor VPN
5353 if (result.resultCode == AppCompatActivity .RESULT_OK ) {
54- startTorAndVpn()
54+ startTorVpn()
55+ } else { /* this happens when:
56+ - the user cancels the system VPN dialog
57+ - the user is on Android S+ and has another VpnService based app
58+ set to be always-on.
59+
60+ we are unable to differentiate these two things, so show a generic error msg
61+ */
62+ ignoreStartSwitchListener = true
63+ binding.switchConnect.isChecked = false
64+ displayVpnStartError(getString(R .string.unable_to_start_unknown_reason_error_msg))
5565 }
5666 }
5767
@@ -102,7 +112,7 @@ class ConnectFragment : Fragment(),
102112 viewLifecycleOwner.lifecycleScope.launch {
103113 viewModel.events.collect { event ->
104114 when (event) {
105- is ConnectEvent .StartTorAndVpn -> startTorAndVpn ()
115+ is ConnectEvent .StartTorAndVpn -> attemptToStartTor ()
106116 is ConnectEvent .RefreshMenuList -> refreshMenuList(requireContext())
107117 }
108118 }
@@ -118,24 +128,27 @@ class ConnectFragment : Fragment(),
118128 }, DEFAULT_THROTTLE_INTERVAL )
119129 }
120130 binding.switchConnect.setOnCheckedChangeListener { _, value ->
131+ if (ignoreStartSwitchListener) {
132+ ignoreStartSwitchListener = false
133+ return @setOnCheckedChangeListener
134+ }
121135 if (value) {
122136 // display msg if optional outbound proxy config is invalid
123137 if (Prefs .outboundProxy.second != null ) {
124-
125138 Toast .makeText(
126139 activity,
127140 getString(R .string.invalid_outbound_proxy_config),
128141 Toast .LENGTH_LONG
129142 ).show()
130143 }
131- startTorAndVpn()
132- } else
133- stopTorAndVpn()
144+ }
145+ attemptToStartTor()
134146 }
135147 refreshMenuList(requireContext())
136-
137148 }
138149
150+ private var ignoreStartSwitchListener = false
151+
139152 override fun onCreateView (
140153 inflater : LayoutInflater , container : ViewGroup ? , savedInstanceState : Bundle ?
141154 ): View {
@@ -170,36 +183,56 @@ class ConnectFragment : Fragment(),
170183 }
171184 }
172185
173- fun startTorAndVpn () {
174- val vpnIntent = VpnService .prepare(requireActivity())?.putNotSystem()
175- if (vpnIntent != null && ! Prefs .isPowerUserMode) {
176- // prompt VPN permission dialog
177- startTorResultLauncher.launch(vpnIntent)
178- } else { // either the vpn permission hasn't been granted or we are in power user mode
179- Prefs .putUseVpn(! Prefs .isPowerUserMode)
180- if (Prefs .isPowerUserMode) {
181- // android 14 awkwardly needs this permission to be explicitly granted to use the
182- // FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED permission without grabbing a VPN Intent
183- val alarmManager =
184- requireContext().getSystemService(Context .ALARM_SERVICE ) as AlarmManager
185- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S && ! alarmManager.canScheduleExactAlarms()) {
186- PowerUserForegroundPermDialog ().createTransactionAndShow(requireActivity())
187- return // user can try again after granting permission
188- } else {
189- binding.ivStatus.setImageResource(R .drawable.orbieon)
190- }
186+ fun attemptToStartTorPowerUserMode () {
187+ // android 14 awkwardly needs this permission to be explicitly granted to use the
188+ // FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED permission without grabbing a VPN Intent
189+ val alarmManager =
190+ requireContext().getSystemService(Context .ALARM_SERVICE ) as AlarmManager
191+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S && ! alarmManager.canScheduleExactAlarms()) {
192+ PowerUserForegroundPermDialog ().createTransactionAndShow(requireActivity())
193+ return // user can try again after granting permission
194+ }
195+ doLayoutStarting(requireContext())
196+ setState(TorService .ACTION_START )
197+ }
198+
199+ fun startTorVpn () {
200+ doLayoutStarting(requireContext())
201+ setState(TorService .ACTION_START )
202+ }
203+
204+ fun attemptToStartTor () {
205+ Prefs .putUseVpn(! Prefs .isPowerUserMode)
206+ if (Prefs .isPowerUserMode) {
207+ attemptToStartTorPowerUserMode()
208+ } else {
209+ val vpnPrepareState =
210+ VpnServicePrepareWrapper .orbotVpnServicePreparedState(requireContext())
211+ when (vpnPrepareState) {
212+ is VpnServicePrepareWrapper .Result .Prepared ->
213+ startTorVpn()
214+
215+ is VpnServicePrepareWrapper .Result .CantPrepare ->
216+ displayVpnStartError(vpnPrepareState.errorMsg)
217+
218+ is VpnServicePrepareWrapper .Result .ShouldAttempt ->
219+ // prompt VPN permission dialog
220+ startTorVpnResultLauncher.launch(vpnPrepareState.prepareIntent)
221+
191222 }
192- doLayoutStarting(requireContext())
193- setState(TorService .ACTION_START )
194223 }
195224 refreshMenuList(requireContext())
196225 }
197226
227+ fun displayVpnStartError (msg : String ) {
228+ // TODO to better UI
229+ Toast .makeText(requireContext(), msg, Toast .LENGTH_LONG ).show()
230+ }
231+
198232 var lastState: String? = null
199233
200234 @Synchronized
201235 fun setState (newState : String ) {
202-
203236 if (lastState != newState) {
204237 requireContext().sendIntentToService(newState)
205238 lastState = newState
0 commit comments