From 1a892b2de3ff04398b1fa2b120e2420a3395f263 Mon Sep 17 00:00:00 2001 From: juuce79 <119090114+juuce79@users.noreply.github.com> Date: Sun, 2 Mar 2025 13:30:10 +0100 Subject: [PATCH] Kotlin conversion of AboutActivity with tests also in Kotlin (#2360) --- .../protect/card_locker/AboutActivity.java | 146 --------------- .../java/protect/card_locker/AboutActivity.kt | 149 +++++++++++++++ .../protect/card_locker/AboutActivityTest.kt | 171 ++++++++++++++++++ 3 files changed, 320 insertions(+), 146 deletions(-) delete mode 100644 app/src/main/java/protect/card_locker/AboutActivity.java create mode 100644 app/src/main/java/protect/card_locker/AboutActivity.kt create mode 100644 app/src/test/java/protect/card_locker/AboutActivityTest.kt diff --git a/app/src/main/java/protect/card_locker/AboutActivity.java b/app/src/main/java/protect/card_locker/AboutActivity.java deleted file mode 100644 index 01bceab57..000000000 --- a/app/src/main/java/protect/card_locker/AboutActivity.java +++ /dev/null @@ -1,146 +0,0 @@ -package protect.card_locker; - -import android.os.Bundle; -import android.text.Spanned; -import android.view.MenuItem; -import android.view.View; -import android.widget.ScrollView; -import android.widget.TextView; - -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import protect.card_locker.databinding.AboutActivityBinding; - -public class AboutActivity extends CatimaAppCompatActivity { - - private static final String TAG = "Catima"; - - private AboutActivityBinding binding; - private AboutContent content; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = AboutActivityBinding.inflate(getLayoutInflater()); - content = new AboutContent(this); - setTitle(content.getPageTitle()); - setContentView(binding.getRoot()); - setSupportActionBar(binding.toolbar); - enableToolbarBackButton(); - - TextView copyright = binding.creditsSub; - copyright.setText(content.getCopyrightShort()); - TextView versionHistory = binding.versionHistorySub; - versionHistory.setText(content.getVersionHistory()); - - binding.versionHistory.setTag("https://catima.app/changelog/"); - binding.translate.setTag("https://hosted.weblate.org/engage/catima/"); - binding.license.setTag("https://github.com/CatimaLoyalty/Android/blob/main/LICENSE"); - binding.repo.setTag("https://github.com/CatimaLoyalty/Android/"); - binding.privacy.setTag("https://catima.app/privacy-policy/"); - binding.reportError.setTag("https://github.com/CatimaLoyalty/Android/issues"); - binding.rate.setTag("https://play.google.com/store/apps/details?id=me.hackerchick.catima"); - binding.donate.setTag("https://catima.app/donate"); - - // Hide Google Play rate button if not on Google Play - binding.rate.setVisibility(BuildConfig.showRateOnGooglePlay ? View.VISIBLE : View.GONE); - // Hide donate button on Google Play (Google Play doesn't allow donation links) - binding.donate.setVisibility(BuildConfig.showDonate ? View.VISIBLE : View.GONE); - - bindClickListeners(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - if (id == android.R.id.home) { - finish(); - } - return super.onOptionsItemSelected(item); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - content.destroy(); - clearClickListeners(); - binding = null; - } - - private void bindClickListeners() { - binding.versionHistory.setOnClickListener(this::showHistory); - binding.translate.setOnClickListener(this::openExternalBrowser); - binding.license.setOnClickListener(this::showLicense); - binding.repo.setOnClickListener(this::openExternalBrowser); - binding.privacy.setOnClickListener(this::showPrivacy); - binding.reportError.setOnClickListener(this::openExternalBrowser); - binding.rate.setOnClickListener(this::openExternalBrowser); - binding.donate.setOnClickListener(this::openExternalBrowser); - - binding.credits.setOnClickListener(view -> showCredits()); - } - - private void clearClickListeners() { - binding.versionHistory.setOnClickListener(null); - binding.translate.setOnClickListener(null); - binding.license.setOnClickListener(null); - binding.repo.setOnClickListener(null); - binding.privacy.setOnClickListener(null); - binding.reportError.setOnClickListener(null); - binding.rate.setOnClickListener(null); - binding.donate.setOnClickListener(null); - - binding.credits.setOnClickListener(null); - } - - private void showCredits() { - showHTML(R.string.credits, content.getContributorInfo(), null); - } - - private void showHistory(View view) { - showHTML(R.string.version_history, content.getHistoryInfo(), view); - } - - private void showLicense(View view) { - showHTML(R.string.license, content.getLicenseInfo(), view); - } - - private void showPrivacy(View view) { - showHTML(R.string.privacy_policy, content.getPrivacyInfo(), view); - } - - private void showHTML(@StringRes int title, final Spanned text, @Nullable View view) { - int dialogContentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding); - TextView textView = new TextView(this); - textView.setText(text); - Utils.makeTextViewLinksClickable(textView, text); - ScrollView scrollView = new ScrollView(this); - scrollView.addView(textView); - scrollView.setPadding(dialogContentPadding, dialogContentPadding / 2, dialogContentPadding, 0); - - // Create dialog - MaterialAlertDialogBuilder materialAlertDialogBuilder = new MaterialAlertDialogBuilder(this); - materialAlertDialogBuilder - .setTitle(title) - .setView(scrollView) - .setPositiveButton(R.string.ok, null); - - // Add View online button if an URL is linked to this view - if (view != null && view.getTag() != null) { - materialAlertDialogBuilder.setNeutralButton(R.string.view_online, (dialog, which) -> openExternalBrowser(view)); - } - - // Show dialog - materialAlertDialogBuilder.show(); - } - - private void openExternalBrowser(View view) { - Object tag = view.getTag(); - if (tag instanceof String && ((String) tag).startsWith("https://")) { - (new OpenWebLinkHandler()).openBrowser(this, (String) tag); - } - } -} diff --git a/app/src/main/java/protect/card_locker/AboutActivity.kt b/app/src/main/java/protect/card_locker/AboutActivity.kt new file mode 100644 index 000000000..ed3af7da7 --- /dev/null +++ b/app/src/main/java/protect/card_locker/AboutActivity.kt @@ -0,0 +1,149 @@ +package protect.card_locker + +import android.os.Bundle +import android.text.Spanned +import android.view.MenuItem +import android.view.View +import android.widget.ScrollView +import android.widget.TextView + +import androidx.annotation.StringRes +import androidx.core.view.isVisible + +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +import protect.card_locker.databinding.AboutActivityBinding + +class AboutActivity : CatimaAppCompatActivity() { + private companion object { + private const val TAG = "Catima" + } + + private lateinit var binding: AboutActivityBinding + private lateinit var content: AboutContent + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = AboutActivityBinding.inflate(layoutInflater) + content = AboutContent(this) + title = content.pageTitle + setContentView(binding.root) + setSupportActionBar(binding.toolbar) + enableToolbarBackButton() + + binding.apply { + creditsSub.text = content.copyrightShort + versionHistorySub.text = content.versionHistory + + versionHistory.tag = "https://catima.app/changelog/" + translate.tag = "https://hosted.weblate.org/engage/catima/" + license.tag = "https://github.com/CatimaLoyalty/Android/blob/main/LICENSE" + repo.tag = "https://github.com/CatimaLoyalty/Android/" + privacy.tag = "https://catima.app/privacy-policy/" + reportError.tag = "https://github.com/CatimaLoyalty/Android/issues" + rate.tag = "https://play.google.com/store/apps/details?id=me.hackerchick.catima" + donate.tag = "https://catima.app/donate" + + // Hide Google Play rate button if not on Google Play + rate.isVisible = BuildConfig.showRateOnGooglePlay + // Hide donate button on Google Play (Google Play doesn't allow donation links) + donate.isVisible = BuildConfig.showDonate + } + + bindClickListeners() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + finish() + true + } + + else -> super.onOptionsItemSelected(item) + } + } + + override fun onDestroy() { + super.onDestroy() + content.destroy() + clearClickListeners() + } + + private fun bindClickListeners() { + binding.apply { + versionHistory.setOnClickListener { showHistory(it) } + translate.setOnClickListener { openExternalBrowser(it) } + license.setOnClickListener { showLicense(it) } + repo.setOnClickListener { openExternalBrowser(it) } + privacy.setOnClickListener { showPrivacy(it) } + reportError.setOnClickListener { openExternalBrowser(it) } + rate.setOnClickListener { openExternalBrowser(it) } + donate.setOnClickListener { openExternalBrowser(it) } + credits.setOnClickListener { showCredits() } + } + } + + private fun clearClickListeners() { + binding.apply { + versionHistory.setOnClickListener(null) + translate.setOnClickListener(null) + license.setOnClickListener(null) + repo.setOnClickListener(null) + privacy.setOnClickListener(null) + reportError.setOnClickListener(null) + rate.setOnClickListener(null) + donate.setOnClickListener(null) + credits.setOnClickListener(null) + } + } + + private fun showCredits() { + showHTML(R.string.credits, content.contributorInfo, null) + } + + private fun showHistory(view: View) { + showHTML(R.string.version_history, content.historyInfo, view) + } + + private fun showLicense(view: View) { + showHTML(R.string.license, content.licenseInfo, view) + } + + private fun showPrivacy(view: View) { + showHTML(R.string.privacy_policy, content.privacyInfo, view) + } + + private fun showHTML(@StringRes title: Int, text: Spanned, view: View?) { + val dialogContentPadding = resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding) + val textView = TextView(this).apply { + setText(text) + Utils.makeTextViewLinksClickable(this, text) + } + + val scrollView = ScrollView(this).apply { + addView(textView) + setPadding(dialogContentPadding, dialogContentPadding / 2, dialogContentPadding, 0) + } + + MaterialAlertDialogBuilder(this).apply { + setTitle(title) + setView(scrollView) + setPositiveButton(R.string.ok, null) + + // Add View online button if an URL is linked to this view + view?.tag?.let { + setNeutralButton(R.string.view_online) { _, _ -> openExternalBrowser(view) } + } + + show() + } + } + + private fun openExternalBrowser(view: View) { + val tag = view.tag + if (tag is String && tag.startsWith("https://")) { + OpenWebLinkHandler().openBrowser(this, tag) + } + } +} diff --git a/app/src/test/java/protect/card_locker/AboutActivityTest.kt b/app/src/test/java/protect/card_locker/AboutActivityTest.kt new file mode 100644 index 000000000..002f6e009 --- /dev/null +++ b/app/src/test/java/protect/card_locker/AboutActivityTest.kt @@ -0,0 +1,171 @@ +package protect.card_locker + +import android.content.Intent +import android.net.Uri +import android.view.View +import android.widget.TextView +import androidx.core.view.isVisible +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.shadows.ShadowActivity +import org.robolectric.shadows.ShadowLog +import java.lang.reflect.Method + +@RunWith(RobolectricTestRunner::class) +class AboutActivityTest { + private lateinit var activityController: org.robolectric.android.controller.ActivityController + private lateinit var activity: AboutActivity + private lateinit var shadowActivity: ShadowActivity + + @Before + fun setUp() { + ShadowLog.stream = System.out + activityController = Robolectric.buildActivity(AboutActivity::class.java) + activity = activityController.get() + shadowActivity = shadowOf(activity) + } + + @Test + fun testActivityCreation() { + activityController.create().start().resume() + + // Verify activity title is set correctly + assertEquals(activity.title.toString(), + activity.getString(R.string.about_title_fmt, activity.getString(R.string.app_name))) + + // Check key elements are initialized + assertNotNull(activity.findViewById(R.id.toolbar)) + assertNotNull(activity.findViewById(R.id.credits_sub)) + assertNotNull(activity.findViewById(R.id.version_history_sub)) + } + + @Test + fun testDisplayOptionsBasedOnConfig() { + activityController.create().start().resume() + + // Test Google Play rate button visibility based on BuildConfig + val rateButton = activity.findViewById(R.id.rate) + assertEquals(BuildConfig.showRateOnGooglePlay, rateButton.isVisible) + + // Test donate button visibility based on BuildConfig + val donateButton = activity.findViewById(R.id.donate) + assertEquals(BuildConfig.showDonate, donateButton.isVisible) + } + + @Test + fun testClickListeners() { + activityController.create().start().resume() + + // Test clicking on a link that opens external browser + val repoButton = activity.findViewById(R.id.repo) + repoButton.performClick() + + val startedIntent = shadowActivity.nextStartedActivity + assertEquals(Intent.ACTION_VIEW, startedIntent.action) + assertEquals(Uri.parse("https://github.com/CatimaLoyalty/Android/"), + startedIntent.data) + } + + @Test + fun testActivityDestruction() { + activityController.create().start().resume() + + // Verify a view exists before destruction + assertNotNull(activity.findViewById(R.id.credits_sub)) + + activityController.pause().stop().destroy() + + // Verify activity was destroyed + assertTrue(activity.isDestroyed) + } + + @Test + fun testDialogContentMethods() { + activityController.create().start().resume() + + // Use reflection to test private methods + try { + val showCreditsMethod: Method = AboutActivity::class.java.getDeclaredMethod("showCredits") + showCreditsMethod.isAccessible = true + showCreditsMethod.invoke(activity) // Should not throw exception + + val showHistoryMethod: Method = AboutActivity::class.java.getDeclaredMethod("showHistory", View::class.java) + showHistoryMethod.isAccessible = true + showHistoryMethod.invoke(activity, activity.findViewById(R.id.version_history)) // Should not throw exception + } catch (e: Exception) { + fail("Exception when calling dialog methods: ${e.message}") + } + } + + @Test + fun testExternalBrowserWithDifferentURLs() { + activityController.create().start().resume() + + try { + // Get access to the private method + val openExternalBrowserMethod: Method = AboutActivity::class.java.getDeclaredMethod("openExternalBrowser", View::class.java) + openExternalBrowserMethod.isAccessible = true + + // Create test URLs + val testUrls = arrayOf( + "https://hosted.weblate.org/engage/catima/", + "https://github.com/CatimaLoyalty/Android/blob/main/LICENSE", + "https://catima.app/privacy-policy/", + "https://github.com/CatimaLoyalty/Android/issues" + ) + + for (url in testUrls) { + // Create a View with the URL as tag + val testView = View(activity) + testView.tag = url + + // Call the method directly + openExternalBrowserMethod.invoke(activity, testView) + + // Verify the intent + val intent = shadowActivity.nextStartedActivity + assertNotNull("No intent launched for URL: $url", intent) + assertEquals(Intent.ACTION_VIEW, intent.action) + assertEquals(Uri.parse(url), intent.data) + } + } catch (e: Exception) { + fail("Exception during reflection: ${e.message}") + } + } + + @Test + fun testButtonVisibilityBasedOnBuildConfig() { + activityController.create().start().resume() + + // Get the current values from BuildConfig + val showRateOnGooglePlay = BuildConfig.showRateOnGooglePlay + val showDonate = BuildConfig.showDonate + + // Test that the visibility matches the BuildConfig values + assertEquals(showRateOnGooglePlay, activity.findViewById(R.id.rate).isVisible) + assertEquals(showDonate, activity.findViewById(R.id.donate).isVisible) + } + + @Test + fun testAboutScreenTextContent() { + activityController.create().start().resume() + + // Verify that text fields contain the expected content + val creditsSub = activity.findViewById(R.id.credits_sub) + assertNotNull(creditsSub.text) + assertFalse(creditsSub.text.toString().isEmpty()) + + val versionHistorySub = activity.findViewById(R.id.version_history_sub) + assertNotNull(versionHistorySub.text) + assertFalse(versionHistorySub.text.toString().isEmpty()) + } +}