From 3afb9878ac5174623b2ee33312db8f7d6332fcea Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Tue, 11 Aug 2026 12:39:23 -0700 Subject: [PATCH 1/2] ADFA-5097: Clean up telemetry choice screen The consent dialog was a stock MaterialAlertDialog built from setTitle + setMessage + three buttons. Every complaint in the ticket was that widget's behavior once the message overflowed at large font scale: Material draws scroll-indicator dividers around the message pane, the borderless text buttons read as plain text, and the message scrolls while the button bar stays fixed. On the reported device the button bar was pushed off-screen entirely, leaving "Keep offline" half-cut and "Learn more" invisible - the decline option was unreachable on a non-cancelable dialog. Styling could not fix that, so the dialog now takes a custom view holding the body and both choices in one NestedScrollView: - No dividers. Material only draws them around the message pane, and there is no longer a message. - Choices are real filled/outlined MaterialButtons, full width, stacked. - Body and choices scroll together, and the scrollbar is non-fading so scrollability is visible before the user touches anything. - The choice panel sits on colorSurfaceVariant to set it off from the body. Not a surfaceContainer role: this app's themes define colorSurfaceVariant but leave the container roles to the Material3 defaults, and a Material3 dialog is itself colorSurfaceContainerHigh, so such a panel would be invisible. Copy condensed to two paragraphs, dropping the Firebase/GlitchTip names and the privacy-policy paragraph. At font scale 2.0 it now fits without scrolling at all. "Learn more" is removed. It opened an off-device PDF via ACTION_VIEW - exactly what the app tries to avoid - and the content is legal boilerplate already on the website. privacy_policy_url had no other consumer and is deleted too. Also retranslates the zh-rCN and in-rID strings, which were not merely stale: both were missing privacy_disclosure_decline entirely and rendered "accept" as "I understand" / "Saya mengerti", so those users saw a one-button dialog whose button said something the English never said. Verified on an arm64 emulator at font scale 1.0 and 2.0, plus 3.0 to force overflow and confirm the scrollbar appears and both buttons scroll into reach. Light and dark themes checked; accept persists GRANTED and decline persists DECLINED. --- .../helper/HandlePrivacyDisclosureHelper.kt | 6 --- .../onboarding/PermissionsFragment.kt | 54 +++++++++---------- .../layout/layout_dialog_privacy_consent.xml | 54 +++++++++++++++++++ .../src/main/res/values-in-rID/strings.xml | 6 +-- .../src/main/res/values-zh-rCN/strings.xml | 7 ++- resources/src/main/res/values/strings.xml | 4 +- 6 files changed, 87 insertions(+), 44 deletions(-) create mode 100644 app/src/main/res/layout/layout_dialog_privacy_consent.xml diff --git a/app/src/androidTest/kotlin/com/itsaky/androidide/helper/HandlePrivacyDisclosureHelper.kt b/app/src/androidTest/kotlin/com/itsaky/androidide/helper/HandlePrivacyDisclosureHelper.kt index ceded7d292..9a766b8765 100644 --- a/app/src/androidTest/kotlin/com/itsaky/androidide/helper/HandlePrivacyDisclosureHelper.kt +++ b/app/src/androidTest/kotlin/com/itsaky/androidide/helper/HandlePrivacyDisclosureHelper.kt @@ -38,8 +38,6 @@ fun TestContext.handlePrivacyDisclosure() { val d = device.uiDevice val acceptText = targetContext.getString(ResourcesR.string.privacy_disclosure_accept) val declineText = targetContext.getString(ResourcesR.string.privacy_disclosure_decline) - val learnMoreText = - targetContext.getString(ResourcesR.string.privacy_disclosure_learn_more) assertTrue( "Dialog title missing", @@ -52,10 +50,6 @@ fun TestContext.handlePrivacyDisclosure() { "Keep offline button missing", d.findObject(UiSelector().text(declineText)).exists(), ) - assertTrue( - "Learn more button missing", - d.findObject(UiSelector().text(learnMoreText)).exists(), - ) clickFirstAccessibilityNodeByText(acceptText) d.waitForIdle() diff --git a/app/src/main/java/com/itsaky/androidide/fragments/onboarding/PermissionsFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/onboarding/PermissionsFragment.kt index 13b8ba7b12..577d77529f 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/onboarding/PermissionsFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/onboarding/PermissionsFragment.kt @@ -23,13 +23,13 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.provider.Settings +import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.Animation import android.view.animation.AnimationUtils import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog -import androidx.core.net.toUri import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle @@ -38,18 +38,19 @@ import androidx.recyclerview.widget.RecyclerView import com.github.appintro.SlidePolicy import com.github.appintro.SlideSelectionListener import com.google.android.material.button.MaterialButton -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.itsaky.androidide.R import com.itsaky.androidide.activities.OnboardingActivity import com.itsaky.androidide.adapters.onboarding.OnboardingPermissionsAdapter import com.itsaky.androidide.app.DeviceProtectedApplicationLoader import com.itsaky.androidide.app.IDEApplication import com.itsaky.androidide.buildinfo.BuildInfo +import com.itsaky.androidide.databinding.LayoutDialogPrivacyConsentBinding import com.itsaky.androidide.databinding.LayoutOnboardingPermissionsBinding import com.itsaky.androidide.events.InstallationEvent import com.itsaky.androidide.preferences.internal.StatPreferences import com.itsaky.androidide.preferences.internal.TelemetryConsent import com.itsaky.androidide.tasks.doAsyncWithProgress +import com.itsaky.androidide.utils.DialogUtils import com.itsaky.androidide.utils.OverlayPermissionGuide import com.itsaky.androidide.utils.PermissionsHelper import com.itsaky.androidide.utils.flashError @@ -430,36 +431,33 @@ class PermissionsFragment : } private fun showPrivacyDialog() { - privacyDialog = - MaterialAlertDialogBuilder(requireContext()) + val builder = DialogUtils.newMaterialDialogBuilder(requireContext()) + + // Inflate from the builder's themed context so the dialog-scoped attributes the + // layout refers to (body text style, preferred padding) resolve. + val binding = LayoutDialogPrivacyConsentBinding.inflate(LayoutInflater.from(builder.context)) + + val dialog = + builder .setTitle(com.itsaky.androidide.resources.R.string.privacy_disclosure_title) - .setMessage(com.itsaky.androidide.resources.R.string.privacy_disclosure_message) - .setPositiveButton(com.itsaky.androidide.resources.R.string.privacy_disclosure_accept) { dialog, _ -> - StatPreferences.telemetryConsent = TelemetryConsent.GRANTED - DeviceProtectedApplicationLoader.onTelemetryConsentGranted(IDEApplication.instance) - dialog.dismiss() - }.setNegativeButton(com.itsaky.androidide.resources.R.string.privacy_disclosure_decline) { dialog, _ -> - StatPreferences.telemetryConsent = TelemetryConsent.DECLINED - Sentry.close() - dialog.dismiss() - }.setNeutralButton(com.itsaky.androidide.resources.R.string.privacy_disclosure_learn_more, null) + .setView(binding.root) .setCancelable(false) - .show() - .also { dialog -> - dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener { - openPrivacyPolicy() - } - } - } + .create() + + binding.privacyAccept.setOnClickListener { + StatPreferences.telemetryConsent = TelemetryConsent.GRANTED + DeviceProtectedApplicationLoader.onTelemetryConsentGranted(IDEApplication.instance) + dialog.dismiss() + } - private fun openPrivacyPolicy() { - try { - val privacyPolicyUrl = getString(R.string.privacy_policy_url) - val intent = Intent(Intent.ACTION_VIEW, privacyPolicyUrl.toUri()) - startActivity(intent) - } catch (e: Exception) { - Sentry.captureException(e) + binding.privacyDecline.setOnClickListener { + StatPreferences.telemetryConsent = TelemetryConsent.DECLINED + Sentry.close() + dialog.dismiss() } + + privacyDialog = dialog + dialog.show() } private fun enableFinishButton() { diff --git a/app/src/main/res/layout/layout_dialog_privacy_consent.xml b/app/src/main/res/layout/layout_dialog_privacy_consent.xml new file mode 100644 index 0000000000..6cdf16e6c4 --- /dev/null +++ b/app/src/main/res/layout/layout_dialog_privacy_consent.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + diff --git a/resources/src/main/res/values-in-rID/strings.xml b/resources/src/main/res/values-in-rID/strings.xml index 11f7f5a5ba..56194a6d57 100644 --- a/resources/src/main/res/values-in-rID/strings.xml +++ b/resources/src/main/res/values-in-rID/strings.xml @@ -599,9 +599,9 @@ Privasi Privasi & analitik - Code on the Go menggunakan Firebase Analytics dan GlitchTip untuk membantu kami meningkatkan aplikasi.\n\nFirebase Analytics mengumpulkan data penggunaan anonim untuk membantu kami memahami bagaimana aplikasi digunakan.\n\nGlitchTip membantu kami melacak dan memperbaiki masalah.\n\nTidak ada informasi pribadi yang dikumpulkan atau dibagikan. Semua data diproses sesuai dengan kebijakan privasi kami. - Saya mengerti - Pelajari lebih lanjut + Code on the Go mengumpulkan informasi penggunaan dan laporan kerusakan anonim untuk membantu kami memperbaiki bug dan meningkatkan aplikasi. Tidak ada informasi pribadi yang dikumpulkan atau dibagikan.\n\nBagikan data anonim akan mengirim informasi ini. Tetap offline tidak mengirim apa pun. + Bagikan data anonim + Tetap offline ID Unik Perangkat diff --git a/resources/src/main/res/values-zh-rCN/strings.xml b/resources/src/main/res/values-zh-rCN/strings.xml index 364e73a968..75cefcdf1e 100644 --- a/resources/src/main/res/values-zh-rCN/strings.xml +++ b/resources/src/main/res/values-zh-rCN/strings.xml @@ -659,9 +659,9 @@ 隐私 隐私和分析 - Code on the Go 使用 Firebase Analytics 和 GlitchTip 来帮助我们改进应用 \n\nFirebase Analytics 收集匿名使用数据,帮助我们了解应用的使用情况 \n\nGlitchTip 帮助我们跟踪和修复错误 \n\n不会收集或分享任何个人信息 所有数据均按照我们的隐私政策进行处理 - 我了解 - 了解更多 + Code on the Go 会收集匿名的使用情况和崩溃信息,以帮助我们修复缺陷并改进应用。我们不会收集或分享任何个人信息。\n\n选择“共享匿名数据”将发送这些信息;选择“保持离线”则不会发送任何内容。 + 共享匿名数据 + 保持离线 唯一 ID 设备 @@ -1184,7 +1184,6 @@ 支持我们的工作 https://github.com/sponsors/appdevforall http://localhost:6174/i/index.html - https://www.appdevforall.org/wp-content/uploads/2024/08/privacy_notice.pdf http://localhost:6174/i/cogo-quickstart.html info@appdevforall.org mailto:info@appdevforall.org diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index 64f3a3e41b..c8bf5c2127 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -664,10 +664,9 @@ Privacy Privacy & analytics - Code on the Go uses Firebase Analytics and GlitchTip to help us improve the app.\n\nFirebase Analytics collects anonymous usage data to help us understand how the app is used. \n\nGlitchTip helps us track and fix errors.\n\nNo personal information is collected or shared. All data is processed in accordance with our privacy policy.\n\nChoose whether to share this anonymous data. If you choose Keep offline, analytics and crash reports are never sent. + Code on the Go collects anonymous usage and crash information to help us fix bugs and improve the app. No personal information is collected or shared.\n\nShare anonymous data sends this information. Keep offline sends nothing at all. Share anonymous data Keep offline - Learn more Unique ID Device @@ -1216,7 +1215,6 @@ Support our work https://github.com/sponsors/appdevforall http://localhost:6174/i/index.html - https://www.appdevforall.org/wp-content/uploads/2024/08/privacy_notice.pdf http://localhost:6174/i/cogo-quickstart.html info@appdevforall.org mailto:info@appdevforall.org From a1f0758cf63abded83e95ad401d26d9ee7b7f772 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Tue, 11 Aug 2026 13:36:47 -0700 Subject: [PATCH 2/2] ADFA-5097: Italicize the choice names in the consent body Paragraph 2 names the two choices; setting them in italics makes them stand out and hints at the buttons below. Inline markup in the string resource. The layout binds via android:text, so TextView picks up the style spans directly - getString() would have stripped them, but nothing here calls it. Not applied to zh-rCN: synthetic obliquing of Han characters renders poorly, and that translation already sets the two choice names off with the conventional CJK quotation marks. --- resources/src/main/res/values-in-rID/strings.xml | 2 +- resources/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/src/main/res/values-in-rID/strings.xml b/resources/src/main/res/values-in-rID/strings.xml index 56194a6d57..5a4bd3e069 100644 --- a/resources/src/main/res/values-in-rID/strings.xml +++ b/resources/src/main/res/values-in-rID/strings.xml @@ -599,7 +599,7 @@ Privasi Privasi & analitik - Code on the Go mengumpulkan informasi penggunaan dan laporan kerusakan anonim untuk membantu kami memperbaiki bug dan meningkatkan aplikasi. Tidak ada informasi pribadi yang dikumpulkan atau dibagikan.\n\nBagikan data anonim akan mengirim informasi ini. Tetap offline tidak mengirim apa pun. + Code on the Go mengumpulkan informasi penggunaan dan laporan kerusakan anonim untuk membantu kami memperbaiki bug dan meningkatkan aplikasi. Tidak ada informasi pribadi yang dikumpulkan atau dibagikan.\n\nBagikan data anonim akan mengirim informasi ini. Tetap offline tidak mengirim apa pun. Bagikan data anonim Tetap offline diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index c8bf5c2127..33f210b86b 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -664,7 +664,7 @@ Privacy Privacy & analytics - Code on the Go collects anonymous usage and crash information to help us fix bugs and improve the app. No personal information is collected or shared.\n\nShare anonymous data sends this information. Keep offline sends nothing at all. + Code on the Go collects anonymous usage and crash information to help us fix bugs and improve the app. No personal information is collected or shared.\n\nShare anonymous data sends this information. Keep offline sends nothing at all. Share anonymous data Keep offline