Skip to content

Commit 98ef7b8

Browse files
committed
Allow selecting the ports to intercept
1 parent 3b8f753 commit 98ef7b8

17 files changed

+432
-46
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@
8787
android:name="android.support.PARENT_ACTIVITY"
8888
android:value=".MainActivity" />
8989
</activity>
90-
<activity android:name=".ApplicationListActivity"/>
90+
91+
<activity android:name=".ApplicationListActivity" />
92+
<activity android:name=".PortListActivity" />
9193

9294
<meta-data android:name="search-engine" android:resource="@xml/noindex" />
9395
</application>

app/src/main/java/tech/httptoolkit/android/ApplicationListActivity.kt

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import android.widget.PopupMenu
1111
import androidx.appcompat.app.AppCompatActivity
1212
import androidx.core.widget.doAfterTextChanged
1313
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
14-
import kotlinx.android.synthetic.main.activity_app_list.*
14+
import kotlinx.android.synthetic.main.apps_list.*
1515
import kotlinx.coroutines.*
1616
import java.util.*
1717
import kotlin.collections.ArrayList
@@ -33,10 +33,9 @@ class ApplicationListActivity : AppCompatActivity(), SwipeRefreshLayout.OnRefres
3333

3434
override fun onCreate(savedInstanceState: Bundle?) {
3535
super.onCreate(savedInstanceState)
36-
setContentView(R.layout.activity_app_list)
36+
setContentView(R.layout.apps_list)
3737

38-
blockedPackages = intent.getStringArrayExtra(UNSELECTED_APPS_EXTRA)?.toHashSet()
39-
?: HashSet()
38+
blockedPackages = intent.getStringArrayExtra(UNSELECTED_APPS_EXTRA)!!.toHashSet()
4039

4140
apps_list_recyclerView.adapter =
4241
ApplicationListAdapter(
@@ -164,12 +163,10 @@ class ApplicationListActivity : AppCompatActivity(), SwipeRefreshLayout.OnRefres
164163
}
165164

166165
override fun onBackPressed() {
167-
setResult(RESULT_OK, Intent().let { intent ->
168-
intent.putExtra(
169-
UNSELECTED_APPS_EXTRA,
170-
blockedPackages.toTypedArray()
171-
)
172-
})
166+
setResult(RESULT_OK, Intent().putExtra(
167+
UNSELECTED_APPS_EXTRA,
168+
blockedPackages.toTypedArray()
169+
))
173170
finish()
174171
}
175172
}

app/src/main/java/tech/httptoolkit/android/ConnectionStatusView.kt

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ class ConnectionStatusView(
1818
proxyConfig: ProxyConfig,
1919
totalAppCount: Int,
2020
interceptedAppCount: Int,
21-
changeApps: () -> Unit
21+
changeApps: () -> Unit,
22+
interceptedPorts: Set<Int>,
23+
changePorts: () -> Unit
2224
) : LinearLayout(context) {
2325

2426
init {
@@ -50,7 +52,7 @@ class ConnectionStatusView(
5052
)
5153
}
5254

53-
val appInterceptionStatus = findViewById<MaterialCardView>(R.id.appInterceptionStatus)
55+
val appInterceptionStatus = findViewById<MaterialCardView>(R.id.interceptedAppsButton)
5456
appInterceptionStatus.setOnClickListener { _ ->
5557
if (!isLineageOs) {
5658
changeApps()
@@ -76,14 +78,30 @@ class ConnectionStatusView(
7678
}
7779
}
7880

79-
val appInterceptionStatusText = findViewById<TextView>(R.id.appInterceptionStatusText)
81+
val appInterceptionStatusText = findViewById<TextView>(R.id.interceptedAppsStatus)
8082
appInterceptionStatusText.text = context.getString(
8183
when {
82-
totalAppCount == interceptedAppCount -> R.string.intercepting_all
83-
interceptedAppCount > 10 -> R.string.intercepting_selected
84-
else -> R.string.intercepting_few
85-
}
86-
, interceptedAppCount, if (interceptedAppCount != 1) "s" else "")
84+
totalAppCount == interceptedAppCount -> R.string.all_apps
85+
interceptedAppCount > 10 -> R.string.selected_apps
86+
else -> R.string.few_apps
87+
},
88+
interceptedAppCount,
89+
if (interceptedAppCount != 1) "s" else ""
90+
)
91+
92+
val portInterceptionStatus = findViewById<MaterialCardView>(R.id.interceptedPortsButton)
93+
portInterceptionStatus.setOnClickListener { _ -> changePorts() }
94+
95+
val portInterceptionStatusText = findViewById<TextView>(R.id.interceptedPortsStatus)
96+
portInterceptionStatusText.text = context.getString(
97+
when {
98+
(interceptedPorts == DEFAULT_PORTS) -> R.string.default_ports
99+
interceptedPorts.size > 10 -> R.string.selected_ports
100+
else -> R.string.few_ports
101+
},
102+
interceptedPorts.size,
103+
if (interceptedPorts.size != 1) "s" else ""
104+
)
87105
}
88106

89107
}

app/src/main/java/tech/httptoolkit/android/HttpToolkitApplication.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,18 @@ class HttpToolkitApplication : Application() {
201201
prefs.edit().putStringSet("unintercepted-packages", packageNames).apply()
202202
}
203203

204+
var interceptedPorts: Set<Int>
205+
get() {
206+
val prefs = getSharedPreferences("tech.httptoolkit.android", MODE_PRIVATE)
207+
val portsSet = prefs.getStringSet("intercepted-ports", null)
208+
return portsSet?.map(String::toInt)?.toSortedSet()
209+
?: DEFAULT_PORTS
210+
}
211+
set(ports) {
212+
val prefs = getSharedPreferences("tech.httptoolkit.android", MODE_PRIVATE)
213+
prefs.edit().putStringSet("intercepted-ports", ports.map(Int::toString).toSet()).apply()
214+
}
215+
204216
fun trackScreen(name: String) {
205217
ga?.setScreenName(name)
206218
ga?.send(HitBuilders.EventBuilder().build())

app/src/main/java/tech/httptoolkit/android/MainActivity.kt

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const val START_VPN_REQUEST = 123
4040
const val INSTALL_CERT_REQUEST = 456
4141
const val SCAN_REQUEST = 789
4242
const val PICK_APPS_REQUEST = 499
43+
const val PICK_PORTS_REQUEST = 443
4344

4445
enum class MainState {
4546
DISCONNECTED,
@@ -268,7 +269,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
268269
proxyConfig,
269270
totalAppCount,
270271
interceptedAppsCount,
271-
::chooseApps
272+
::chooseApps,
273+
app.interceptedPorts,
274+
::choosePorts
272275
)
273276
)
274277

@@ -434,15 +437,24 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
434437
launchBrowser("httptoolkit.tech/docs/guides/android")
435438
}
436439

437-
private fun chooseApps(){
440+
private fun chooseApps() {
438441
startActivityForResult(
439-
Intent(this,ApplicationListActivity::class.java).apply {
442+
Intent(this, ApplicationListActivity::class.java).apply {
440443
putExtra(UNSELECTED_APPS_EXTRA, app.uninterceptedApps.toTypedArray())
441444
},
442445
PICK_APPS_REQUEST
443446
)
444447
}
445448

449+
private fun choosePorts() {
450+
startActivityForResult(
451+
Intent(this, PortListActivity::class.java).apply {
452+
putExtra(SELECTED_PORTS_EXTRA, app.interceptedPorts.toIntArray())
453+
},
454+
PICK_PORTS_REQUEST
455+
)
456+
}
457+
446458
private fun testInterception() {
447459
app.trackEvent("Button", "test-interception")
448460

@@ -493,6 +505,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
493505
INSTALL_CERT_REQUEST -> "install-cert"
494506
SCAN_REQUEST -> "scan-request"
495507
PICK_APPS_REQUEST -> "pick-apps"
508+
PICK_PORTS_REQUEST -> "pick-ports"
496509
else -> requestCode.toString()
497510
})
498511

@@ -518,6 +531,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
518531
app.uninterceptedApps = unselectedApps
519532
if (isVpnActive()) startVpn()
520533
}
534+
} else if (requestCode == PICK_PORTS_REQUEST) {
535+
app.trackEvent("Setup", "picked-ports")
536+
val selectedPorts = data!!.getIntArrayExtra(SELECTED_PORTS_EXTRA)!!.toSet()
537+
if (selectedPorts != app.interceptedPorts) {
538+
app.interceptedPorts = selectedPorts
539+
if (isVpnActive()) startVpn()
540+
}
521541
}
522542
} else if (
523543
requestCode == START_VPN_REQUEST &&
@@ -553,6 +573,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
553573
action = START_VPN_ACTION
554574
putExtra(PROXY_CONFIG_EXTRA, currentProxyConfig)
555575
putExtra(UNINTERCEPTED_APPS_EXTRA, app.uninterceptedApps.toTypedArray())
576+
putExtra(INTERCEPTED_PORTS_EXTRA, app.interceptedPorts.toIntArray())
556577
})
557578
}
558579

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package tech.httptoolkit.android
2+
3+
import android.text.InputFilter
4+
import android.text.Spanned
5+
6+
class MinMaxInputFilter(private val min: Int, private val max: Int) : InputFilter {
7+
8+
override fun filter(
9+
source: CharSequence,
10+
start: Int,
11+
end: Int,
12+
dest: Spanned,
13+
dstart: Int,
14+
dend: Int
15+
): CharSequence? {
16+
try {
17+
val replacement = source.subSequence(start, end).toString()
18+
val updatedValue = dest.subSequence(0, dstart).toString() +
19+
replacement +
20+
dest.subSequence(dend, dest.length)
21+
if (updatedValue.toInt() in min..max) {
22+
return null // Allow the update
23+
}
24+
} catch (nfe: NumberFormatException) { }
25+
26+
return "" // Reject the update
27+
}
28+
29+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package tech.httptoolkit.android
2+
3+
import android.content.Intent
4+
import android.os.Bundle
5+
import android.view.KeyEvent
6+
import android.widget.TextView
7+
import androidx.appcompat.app.AppCompatActivity
8+
import androidx.core.widget.doAfterTextChanged
9+
import kotlinx.android.synthetic.main.item_port_row.view.*
10+
import kotlinx.android.synthetic.main.ports_list.*
11+
import kotlinx.coroutines.*
12+
import java.util.HashSet
13+
import kotlin.collections.ArrayList
14+
15+
val DEFAULT_PORTS = setOf(
16+
80, // HTTP
17+
443, // HTTPS
18+
8000, 8001, 8080, 8888, 9000 // Common local dev ports
19+
)
20+
21+
const val MIN_PORT = 1
22+
const val MAX_PORT = 65535
23+
24+
// Used to both to send and return the current list of selected ports
25+
const val SELECTED_PORTS_EXTRA = "tech.httptoolkit.android.SELECTED_PORTS_EXTRA"
26+
27+
class PortListActivity : AppCompatActivity(), CoroutineScope by MainScope() {
28+
29+
private lateinit var ports: MutableSet<Int>
30+
31+
override fun onCreate(savedInstanceState: Bundle?) {
32+
super.onCreate(savedInstanceState)
33+
setContentView(R.layout.ports_list)
34+
35+
ports = intent.getIntArrayExtra(SELECTED_PORTS_EXTRA)!!.toMutableSet()
36+
37+
ports_list_recyclerView.adapter =
38+
PortListAdapter(
39+
ports,
40+
::deletePort
41+
)
42+
43+
ports_list_input.filters = arrayOf(MinMaxInputFilter(MIN_PORT, MAX_PORT))
44+
45+
ports_list_add_button.isEnabled = false
46+
ports_list_input.doAfterTextChanged {
47+
ports_list_add_button.isEnabled = isValidInput(it.toString())
48+
}
49+
50+
ports_list_add_button.setOnClickListener { addEnteredPort() }
51+
ports_list_input.setOnEditorActionListener { _, _, _ ->
52+
addEnteredPort()
53+
return@setOnEditorActionListener true
54+
}
55+
}
56+
57+
private fun isValidInput(input: String): Boolean =
58+
input.toIntOrNull() != null &&
59+
!ports.contains(input.toInt())
60+
61+
private fun addEnteredPort() {
62+
if (!isValidInput(ports_list_input.text.toString())) return
63+
64+
ports.add(ports_list_input.text.toString().toInt())
65+
ports_list_input.text.clear()
66+
updateList()
67+
}
68+
69+
private fun deletePort(port: Int) {
70+
ports.remove(port)
71+
updateList()
72+
}
73+
74+
private fun updateList() {
75+
ports_list_recyclerView.adapter?.notifyDataSetChanged()
76+
}
77+
78+
override fun onBackPressed() {
79+
setResult(RESULT_OK, Intent().putExtra(
80+
SELECTED_PORTS_EXTRA,
81+
ports.toIntArray()
82+
))
83+
finish()
84+
}
85+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package tech.httptoolkit.android
2+
3+
import android.view.LayoutInflater
4+
import android.view.View
5+
import android.view.ViewGroup
6+
import androidx.recyclerview.widget.RecyclerView
7+
import kotlinx.android.synthetic.main.item_port_row.view.*
8+
9+
val PORT_DESCRIPTIONS = mapOf(
10+
80 to "Standard HTTP port",
11+
81 to "Alternate HTTP port",
12+
443 to "Standard HTTPS port",
13+
8000 to "Popular local development port",
14+
8001 to "Popular local development port",
15+
8080 to "Popular local development port",
16+
8090 to "Popular local development port",
17+
8888 to "Popular local development port",
18+
9000 to "Popular local development port"
19+
)
20+
21+
class PortListAdapter(
22+
private val ports: Set<Int>,
23+
private val onPortDeleted: (Int) -> Unit
24+
) : RecyclerView.Adapter<PortListAdapter.PortViewHolder>() {
25+
26+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PortViewHolder {
27+
return PortViewHolder(
28+
LayoutInflater.from(parent.context).inflate(R.layout.item_port_row, parent, false)
29+
)
30+
}
31+
32+
override fun getItemCount() = ports.size
33+
34+
override fun onBindViewHolder(holder: PortViewHolder, position: Int) {
35+
holder.bind(ports.elementAt(position))
36+
}
37+
38+
inner class PortViewHolder(v: View) : RecyclerView.ViewHolder(v) {
39+
40+
init {
41+
itemView.row_port_delete.setOnClickListener { _ ->
42+
onPortDeleted(ports.elementAt(layoutPosition))
43+
}
44+
}
45+
46+
fun bind(port: Int) {
47+
itemView.row_port.text = port.toString()
48+
itemView.row_port_description.text = PORT_DESCRIPTIONS[port]
49+
?: "Unknown port";
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)