Kotlin conversion of AboutActivity with tests also in Kotlin (#2360)

This commit is contained in:
juuce79
2025-03-02 13:30:10 +01:00
committed by GitHub
parent 7719a2d3fd
commit 1a892b2de3
3 changed files with 320 additions and 146 deletions

View File

@@ -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);
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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<AboutActivity>
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<View>(R.id.rate)
assertEquals(BuildConfig.showRateOnGooglePlay, rateButton.isVisible)
// Test donate button visibility based on BuildConfig
val donateButton = activity.findViewById<View>(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<View>(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<View>(R.id.rate).isVisible)
assertEquals(showDonate, activity.findViewById<View>(R.id.donate).isVisible)
}
@Test
fun testAboutScreenTextContent() {
activityController.create().start().resume()
// Verify that text fields contain the expected content
val creditsSub = activity.findViewById<TextView>(R.id.credits_sub)
assertNotNull(creditsSub.text)
assertFalse(creditsSub.text.toString().isEmpty())
val versionHistorySub = activity.findViewById<TextView>(R.id.version_history_sub)
assertNotNull(versionHistorySub.text)
assertFalse(versionHistorySub.text.toString().isEmpty())
}
}