@@ -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 }
0 commit comments