Skip to content

Commit c9a1ed4

Browse files
authored
Merge pull request #31 from Rufim/fix_email_data
Enable Android TV support & allow skipping certificate setup
2 parents 2f3bdad + bb41a93 commit c9a1ed4

File tree

4 files changed

+71
-49
lines changed

4 files changed

+71
-49
lines changed

app/src/main/AndroidManifest.xml

+8-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
2020
tools:node="remove" />
2121

22+
<uses-feature android:name="android.software.leanback"
23+
android:required="false" />
24+
<uses-feature android:name="android.hardware.touchscreen"
25+
android:required="false" />
26+
2227
<application
2328
android:name=".HttpToolkitApplication"
2429
android:allowBackup="true"
@@ -28,7 +33,8 @@
2833
android:supportsRtl="true"
2934
android:theme="@style/AppTheme"
3035
tools:targetApi="m" android:usesCleartextTraffic="true"
31-
android:largeHeap="true">
36+
android:largeHeap="true"
37+
android:banner="@drawable/ic_tv_banner">
3238
<service
3339
android:name=".ProxyVpnService"
3440
android:permission="android.permission.BIND_VPN_SERVICE"
@@ -46,6 +52,7 @@
4652
<intent-filter>
4753
<action android:name="android.intent.action.MAIN" />
4854
<category android:name="android.intent.category.LAUNCHER" />
55+
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
4956
</intent-filter>
5057

5158
<intent-filter

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

+61-46
Original file line numberDiff line numberDiff line change
@@ -237,11 +237,18 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
237237
when (mainState) {
238238
MainState.DISCONNECTED -> {
239239
statusText.setText(R.string.disconnected_status)
240+
buttonContainer.visibility = View.VISIBLE
240241

241-
detailContainer.addView(detailText(R.string.disconnected_details))
242+
val hasCamera = this.packageManager
243+
.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
242244

243-
buttonContainer.visibility = View.VISIBLE
244-
buttonContainer.addView(primaryButton(R.string.scan_button, ::scanCode))
245+
if (hasCamera) {
246+
detailContainer.addView(detailText(R.string.disconnected_details))
247+
val scanQrButton = primaryButton(R.string.scan_button, ::scanCode)
248+
buttonContainer.addView(scanQrButton)
249+
} else {
250+
detailContainer.addView(detailText(R.string.disconnected_no_camera_details))
251+
}
245252

246253
val lastProxy = app.lastProxy
247254
if (lastProxy != null) {
@@ -335,41 +342,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
335342
Log.i(TAG, if (vpnIntent != null) "got intent" else "no intent")
336343
val vpnNotConfigured = vpnIntent != null
337344

338-
if (whereIsCertTrusted(config) == null && PROMPTED_CERT_SETUP_SUPPORTED) {
339-
// The cert isn't trusted, and the VPN may need setup, so there'll be a series of prompts
340-
// here. Explain them beforehand, so users understand what's going on.
341-
withContext(Dispatchers.Main) {
342-
MaterialAlertDialogBuilder(this@MainActivity)
343-
.setTitle("Enable interception")
344-
.setIcon(R.drawable.ic_info_circle)
345-
.setMessage(
346-
"To intercept traffic from this device, you need to " +
347-
(if (vpnNotConfigured) "activate HTTP Toolkit's VPN and " else "") +
348-
"trust your HTTP Toolkit's certificate authority. " +
349-
"\n\n" +
350-
"Please accept the following prompts to allow this." +
351-
if (!isDeviceSecured(applicationContext))
352-
"\n\n" +
353-
"Due to Android security requirements, trusting the certificate will " +
354-
"require you to set a PIN, password or pattern for this device."
355-
else " To trust the certificate, your device PIN will be required."
356-
)
357-
.setPositiveButton("Ok") { _, _ ->
358-
if (vpnNotConfigured) {
359-
startActivityForResult(vpnIntent, START_VPN_REQUEST)
360-
} else {
361-
onActivityResult(START_VPN_REQUEST, RESULT_OK, null)
362-
}
363-
}
364-
.show()
365-
}
366-
} else if (vpnNotConfigured) {
367-
// In this case the VPN needs setup, but the cert is trusted already, so it's
368-
// a single confirmation. Pretty clear, no need to explain. This happens if the
369-
// VPN/app was removed from the device in the past, or when using injected system certs.
345+
if (vpnNotConfigured) {
346+
// Show the 'Enable the VPN' prompt
370347
startActivityForResult(vpnIntent, START_VPN_REQUEST)
371348
} else {
372-
// VPN is trusted & cert setup already, lets get to it.
349+
// VPN is trusted already, continue
373350
onActivityResult(START_VPN_REQUEST, RESULT_OK, null)
374351
}
375352

@@ -637,26 +614,56 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
637614
if (existingTrust == null) {
638615
Log.i(TAG, "Certificate not trusted, prompting to install")
639616

640-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
641-
// Android 11+, with no trusted cert: we need to download the cert to Downloads and
642-
// then tell the user how to install it manually:
643-
launch { promptToManuallyInstallCert(proxyConfig.certificate) }
644-
} else {
617+
if (PROMPTED_CERT_SETUP_SUPPORTED) {
645618
// Up until Android 11, we can prompt the user to install the CA cert into the user
646619
// CA store. Notably, if the cert is already installed as a system cert but
647620
// disabled, this will get triggered, and will enable the cert, rather than adding
648621
// a normal user cert.
649-
val certInstallIntent = KeyChain.createInstallIntent()
650-
certInstallIntent.putExtra(EXTRA_NAME, "HTTP Toolkit CA")
651-
certInstallIntent.putExtra(EXTRA_CERTIFICATE, proxyConfig.certificate.encoded)
652-
startActivityForResult(certInstallIntent, INSTALL_CERT_REQUEST)
622+
launch { promptToAutoInstallCert(proxyConfig.certificate) }
623+
} else {
624+
// Android 11+, with no trusted cert: we need to download the cert to Downloads and
625+
// then tell the user how to install it manually:
626+
launch { promptToManuallyInstallCert(proxyConfig.certificate) }
653627
}
654628
} else {
655629
Log.i(TAG, "Certificate already trusted, continuing")
656630
onActivityResult(INSTALL_CERT_REQUEST, RESULT_OK, null)
657631
}
658632
}
659633

634+
private suspend fun promptToAutoInstallCert(certificate: Certificate) {
635+
withContext(Dispatchers.Main) {
636+
MaterialAlertDialogBuilder(this@MainActivity)
637+
.setTitle("Enable HTTPS interception")
638+
.setIcon(R.drawable.ic_info_circle)
639+
.setMessage(
640+
"To intercept HTTPS traffic from this device, you need to " +
641+
"trust your HTTP Toolkit's certificate authority. " +
642+
"\n\n" +
643+
"Please accept the following prompts to allow this." +
644+
if (!isDeviceSecured(applicationContext))
645+
"\n\n" +
646+
"Due to Android security requirements, trusting the certificate will " +
647+
"require you to set a PIN, password or pattern for this device."
648+
else " To trust the certificate, your device PIN will be required."
649+
)
650+
.setPositiveButton("Install") { _, _ ->
651+
val certInstallIntent = KeyChain.createInstallIntent()
652+
certInstallIntent.putExtra(EXTRA_NAME, "HTTP Toolkit CA")
653+
certInstallIntent.putExtra(EXTRA_CERTIFICATE, certificate.encoded)
654+
startActivityForResult(certInstallIntent, INSTALL_CERT_REQUEST)
655+
}
656+
.setNeutralButton("Skip") { _, _ ->
657+
onActivityResult(INSTALL_CERT_REQUEST, RESULT_OK, null)
658+
}
659+
.setNegativeButton("Cancel") { _, _ ->
660+
disconnect()
661+
}
662+
.setCancelable(false)
663+
.show()
664+
}
665+
}
666+
660667
@RequiresApi(Build.VERSION_CODES.Q)
661668
private suspend fun promptToManuallyInstallCert(cert: Certificate, repeatPrompt: Boolean = false) {
662669
if (!repeatPrompt) {
@@ -694,7 +701,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
694701
Html.fromHtml(
695702
"""
696703
<p>
697-
Android ${Build.VERSION.RELEASE} doesn't allow automatic certificate setup.
704+
${
705+
if (PROMPTED_CERT_SETUP_SUPPORTED)
706+
"Automatic certificate installation failed, so it must be done manually."
707+
else
708+
"Android ${Build.VERSION.RELEASE} doesn't allow automatic certificate setup."
709+
}
698710
</p>
699711
<p>
700712
To allow HTTP Toolkit to intercept HTTPS traffic:
@@ -721,6 +733,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
721733
.setPositiveButton("Open security settings") { _, _ ->
722734
startActivityForResult(Intent(Settings.ACTION_SECURITY_SETTINGS), INSTALL_CERT_REQUEST)
723735
}
736+
.setNeutralButton("Skip") { _, _ ->
737+
onActivityResult(INSTALL_CERT_REQUEST, RESULT_OK, null)
738+
}
724739
.setNegativeButton("Cancel") { _, _ ->
725740
disconnect()
726741
}
6.04 KB
Loading

app/src/main/res/values/strings.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
<string name="connected_status">Connected</string>
1212
<string name="failed_status">Oh no!</string>
1313

14-
<string name="disconnected_details">To intercept this device, start HTTP Toolkit on your computer, and activate Android interception there via QR code or using ADB.
15-
</string>
14+
<string name="disconnected_details">To intercept this device, start HTTP Toolkit on your computer, and activate Android interception there via QR code or using ADB.</string>
15+
<string name="disconnected_no_camera_details">To intercept this device, start HTTP Toolkit on your computer, and activate Android interception there using the ADB or Frida options.</string>
1616

1717
<string name="connected_details">to %s on port %d</string>
1818
<string name="connected_tunnel_details">via ADB tunnel</string>

0 commit comments

Comments
 (0)