@@ -237,11 +237,18 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
237
237
when (mainState) {
238
238
MainState .DISCONNECTED -> {
239
239
statusText.setText(R .string.disconnected_status)
240
+ buttonContainer.visibility = View .VISIBLE
240
241
241
- detailContainer.addView(detailText(R .string.disconnected_details))
242
+ val hasCamera = this .packageManager
243
+ .hasSystemFeature(PackageManager .FEATURE_CAMERA_ANY )
242
244
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
+ }
245
252
246
253
val lastProxy = app.lastProxy
247
254
if (lastProxy != null ) {
@@ -335,41 +342,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
335
342
Log .i(TAG , if (vpnIntent != null ) " got intent" else " no intent" )
336
343
val vpnNotConfigured = vpnIntent != null
337
344
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
370
347
startActivityForResult(vpnIntent, START_VPN_REQUEST )
371
348
} else {
372
- // VPN is trusted & cert setup already, lets get to it.
349
+ // VPN is trusted already, continue
373
350
onActivityResult(START_VPN_REQUEST , RESULT_OK , null )
374
351
}
375
352
@@ -637,26 +614,56 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
637
614
if (existingTrust == null ) {
638
615
Log .i(TAG , " Certificate not trusted, prompting to install" )
639
616
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 ) {
645
618
// Up until Android 11, we can prompt the user to install the CA cert into the user
646
619
// CA store. Notably, if the cert is already installed as a system cert but
647
620
// disabled, this will get triggered, and will enable the cert, rather than adding
648
621
// 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) }
653
627
}
654
628
} else {
655
629
Log .i(TAG , " Certificate already trusted, continuing" )
656
630
onActivityResult(INSTALL_CERT_REQUEST , RESULT_OK , null )
657
631
}
658
632
}
659
633
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
+
660
667
@RequiresApi(Build .VERSION_CODES .Q )
661
668
private suspend fun promptToManuallyInstallCert (cert : Certificate , repeatPrompt : Boolean = false) {
662
669
if (! repeatPrompt) {
@@ -694,7 +701,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
694
701
Html .fromHtml(
695
702
"""
696
703
<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
+ }
698
710
</p>
699
711
<p>
700
712
To allow HTTP Toolkit to intercept HTTPS traffic:
@@ -721,6 +733,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
721
733
.setPositiveButton(" Open security settings" ) { _, _ ->
722
734
startActivityForResult(Intent (Settings .ACTION_SECURITY_SETTINGS ), INSTALL_CERT_REQUEST )
723
735
}
736
+ .setNeutralButton(" Skip" ) { _, _ ->
737
+ onActivityResult(INSTALL_CERT_REQUEST , RESULT_OK , null )
738
+ }
724
739
.setNegativeButton(" Cancel" ) { _, _ ->
725
740
disconnect()
726
741
}
0 commit comments