diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 867e7650..95c5769b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: - name: 'HTML ES5 test' run: | npm install -g jshint - java -cp sdk/build/intermediates/javac/release/classes com.hcaptcha.sdk.HCaptchaHtml > sdk/build/hcaptcha-form.html + java -cp sdk/build/intermediates/javac/release/compileReleaseJavaWithJavac/classes com.hcaptcha.sdk.HCaptchaHtml > sdk/build/hcaptcha-form.html jshint --extract=always sdk/build/hcaptcha-form.html - name: 'JitPack Test' run: ./gradlew publishReleasePublicationToMavenLocal diff --git a/CHANGES.md b/CHANGES.md index bc27700d..a8af0021 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # Changelog +# 4.1.0 + +- Feat: preload WebView on `setup` call + # 4.0.5 - compose-sdk: set minSdk to 21 diff --git a/benchmark/src/androidTest/java/com/hcaptcha/sdk/TestHCaptchaHtml.java b/benchmark/src/androidTest/java/com/hcaptcha/sdk/TestHCaptchaHtml.java index ebd9ee7d..33a757b7 100644 --- a/benchmark/src/androidTest/java/com/hcaptcha/sdk/TestHCaptchaHtml.java +++ b/benchmark/src/androidTest/java/com/hcaptcha/sdk/TestHCaptchaHtml.java @@ -2,8 +2,6 @@ import androidx.annotation.NonNull; -import com.hcaptcha.sdk.IHCaptchaHtmlProvider; - class TestHCaptchaHtml implements IHCaptchaHtmlProvider { @Override diff --git a/benchmark/src/androidTest/java/com/hcaptcha/sdk/TestHCaptchaVerifier.java b/benchmark/src/androidTest/java/com/hcaptcha/sdk/TestHCaptchaVerifier.java index 2a4f4f6b..2f5fdc61 100644 --- a/benchmark/src/androidTest/java/com/hcaptcha/sdk/TestHCaptchaVerifier.java +++ b/benchmark/src/androidTest/java/com/hcaptcha/sdk/TestHCaptchaVerifier.java @@ -2,10 +2,12 @@ import android.app.Activity; +import androidx.annotation.NonNull; + public class TestHCaptchaVerifier implements IHCaptchaVerifier { @Override - public void startVerification(Activity activity) { + public void startVerification(@NonNull Activity activity) { // no implementation need for performance measurement } diff --git a/build.gradle b/build.gradle index 77c8e0a8..895364ef 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.4' - classpath 'androidx.benchmark:benchmark-gradle-plugin:1.2.4' + classpath 'com.android.tools.build:gradle:8.7.3' + classpath 'androidx.benchmark:benchmark-gradle-plugin:1.3.3' classpath 'com.slack.keeper:keeper:0.16.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } diff --git a/compose-sdk/build.gradle b/compose-sdk/build.gradle index d89a8414..1b7b0c46 100644 --- a/compose-sdk/build.gradle +++ b/compose-sdk/build.gradle @@ -20,11 +20,11 @@ android { // See https://developer.android.com/studio/publish/versioning // versionCode must be integer and be incremented by one for every new update // android system uses this to prevent downgrades - versionCode 44 + versionCode 45 // version number visible to the user // should follow semantic versioning (See https://semver.org) - versionName "4.0.5" + versionName "4.1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" diff --git a/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaCompose.kt b/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaCompose.kt index 8c9238c6..553af841 100644 --- a/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaCompose.kt +++ b/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaCompose.kt @@ -1,12 +1,14 @@ package com.hcaptcha.sdk -import android.app.Activity import android.os.Handler import android.os.Looper import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog @@ -14,61 +16,28 @@ import androidx.compose.ui.window.DialogProperties @Composable public fun HCaptchaCompose(config: HCaptchaConfig, onResult: (HCaptchaResponse) -> Unit) { - val handler = Handler(Looper.getMainLooper()) - var helper: HCaptchaWebViewHelper? = null - val verifier = object : IHCaptchaVerifier { - override fun onLoaded() { - onResult(HCaptchaResponse.Event(HCaptchaEvent.Loaded)) - if (config.hideDialog) { - helper?.let { - it.resetAndExecute() - } ?: run { - HCaptchaLog.w("HCaptchaWebViewHelper wasn't created, report but to developer") - onResult(HCaptchaResponse.Failure(HCaptchaError.INTERNAL_ERROR)) - } - } - } - - override fun onOpen() { - onResult(HCaptchaResponse.Event(HCaptchaEvent.Opened)) - } - - override fun onSuccess(result: String) { - onResult(HCaptchaResponse.Success(result)) - } - - override fun onFailure(exception: HCaptchaException) { - onResult(HCaptchaResponse.Failure(exception.hCaptchaError)) - } + HCaptchaLog.sDiagnosticsLogEnabled = config.diagnosticLog - override fun startVerification(activity: Activity) { - error("startVerification should never be reached") - } + val context = LocalContext.current + val handler = Handler(Looper.getMainLooper()) + val internalConfig = HCaptchaInternalConfig(com.hcaptcha.sdk.HCaptchaHtml()) - override fun reset() { - error("reset should never be reached") + val helper = remember { mutableStateOf(null) } + val verifier = remember { HCaptchaComposeVerifier(config, onResult, helper) } + val preloadedWebView = remember { + HCaptchaWebView(context).apply { + helper.value = HCaptchaWebViewHelper( + handler, context, config, internalConfig, verifier, this + ) } } - val internalConfig = HCaptchaInternalConfig(com.hcaptcha.sdk.HCaptchaHtml()) - HCaptchaLog.sDiagnosticsLogEnabled = config.diagnosticLog HCaptchaLog.d("HCaptchaCompose($config)") if (config.hideDialog) { AndroidView( modifier = Modifier.size(0.dp), - factory = { context -> - HCaptchaWebView(context).apply { - helper = HCaptchaWebViewHelper( - handler, - context, - config, - internalConfig, - verifier, - this - ) - } - } + factory = { preloadedWebView } ) } else { Dialog( @@ -77,18 +46,7 @@ public fun HCaptchaCompose(config: HCaptchaConfig, onResult: (HCaptchaResponse) ) { AndroidView( modifier = Modifier.fillMaxSize(), - factory = { context -> - HCaptchaWebView(context).apply { - helper = HCaptchaWebViewHelper( - handler, - context, - config, - internalConfig, - verifier, - this - ) - } - } + factory = { preloadedWebView } ) } } diff --git a/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaComposeVerifier.kt b/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaComposeVerifier.kt new file mode 100644 index 00000000..df83232b --- /dev/null +++ b/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaComposeVerifier.kt @@ -0,0 +1,41 @@ +package com.hcaptcha.sdk + +import android.app.Activity +import androidx.compose.runtime.State + +internal class HCaptchaComposeVerifier( + private val config: HCaptchaConfig, + private val onResult: (HCaptchaResponse) -> Unit, + private val helperState: State +) : IHCaptchaVerifier { + + override fun onLoaded() { + onResult(HCaptchaResponse.Event(HCaptchaEvent.Loaded)) + if (config.hideDialog) { + helperState.value?.resetAndExecute() ?: run { + HCaptchaLog.w("HCaptchaWebViewHelper wasn't created, report but to developer") + onResult(HCaptchaResponse.Failure(HCaptchaError.INTERNAL_ERROR)) + } + } + } + + override fun onOpen() { + onResult(HCaptchaResponse.Event(HCaptchaEvent.Opened)) + } + + override fun onSuccess(result: String) { + onResult(HCaptchaResponse.Success(result)) + } + + override fun onFailure(exception: HCaptchaException) { + onResult(HCaptchaResponse.Failure(exception.hCaptchaError)) + } + + override fun startVerification(activity: Activity) { + error("startVerification should never be reached") + } + + override fun reset() { + error("reset should never be reached") + } +} \ No newline at end of file diff --git a/example-compose-app/build.gradle b/example-compose-app/build.gradle index 9a6b4119..a2b8399c 100644 --- a/example-compose-app/build.gradle +++ b/example-compose-app/build.gradle @@ -12,7 +12,7 @@ android { namespace 'com.hcaptcha.example.compose' defaultConfig { - minSdkVersion 23 + minSdkVersion 21 targetSdkVersion intProp("exampleTargetSdkVersion", 35) versionCode 1 versionName "0.0.1" diff --git a/example-compose-app/src/main/java/com/hcaptcha/example/compose/ComposeActivity.kt b/example-compose-app/src/main/java/com/hcaptcha/example/compose/ComposeActivity.kt index 9d912dce..de306d4d 100644 --- a/example-compose-app/src/main/java/com/hcaptcha/example/compose/ComposeActivity.kt +++ b/example-compose-app/src/main/java/com/hcaptcha/example/compose/ComposeActivity.kt @@ -21,6 +21,7 @@ import com.hcaptcha.sdk.HCaptchaCompose import com.hcaptcha.sdk.HCaptchaConfig import com.hcaptcha.sdk.HCaptchaEvent import com.hcaptcha.sdk.HCaptchaResponse +import com.hcaptcha.sdk.HCaptchaSize class ComposeActivity : ComponentActivity() { @@ -33,7 +34,9 @@ class ComposeActivity : ComponentActivity() { var text by remember { mutableStateOf("") } Column( - modifier = Modifier.fillMaxSize().padding(16.dp), + modifier = Modifier.fillMaxSize() + .padding(WindowInsets.systemBars.asPaddingValues()) + .padding(16.dp), verticalArrangement = Arrangement.Bottom ) { TextField( @@ -91,6 +94,7 @@ class ComposeActivity : ComponentActivity() { HCaptchaCompose(HCaptchaConfig .builder() .siteKey("10000000-ffff-ffff-ffff-000000000001") + .size(if (hideDialog) HCaptchaSize.INVISIBLE else HCaptchaSize.NORMAL) .hideDialog(hideDialog) .diagnosticLog(true) .build()) { result -> diff --git a/gradle/shared/code-quality.gradle b/gradle/shared/code-quality.gradle index efe77442..104f8faf 100644 --- a/gradle/shared/code-quality.gradle +++ b/gradle/shared/code-quality.gradle @@ -2,7 +2,7 @@ checkstyle { toolVersion = '8.45.1' } -task checkstyle(type: Checkstyle) { +tasks.register('checkstyle', Checkstyle) { description 'Check code standard' group 'verification' configFile file("${rootDir}/gradle/config/checkstyle.xml") @@ -19,7 +19,7 @@ pmd { toolVersion = "6.51.0" } -task pmd(type: Pmd) { +tasks.register('pmd', Pmd) { ruleSetFiles = files("${project.rootDir}/gradle/config/pmd.xml") ignoreFailures = false ruleSets = [] @@ -56,7 +56,8 @@ gradle.taskGraph.beforeTask { task -> } // https://www.rallyhealth.com/coding/code-coverage-for-android-testing -task jacocoUnitTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { +tasks.register('jacocoUnitTestReport', JacocoReport) { + dependsOn['testDebugUnitTest'] def coverageSourceDirs = [ "src/main/java" ] @@ -79,6 +80,8 @@ task jacocoUnitTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) xml.required = true html.required = true } + + inputs.files(tasks.named("testDebugUnitTest").get().outputs) } check.dependsOn('checkstyle', 'pmd', 'jacocoUnitTestReport') @@ -93,16 +96,16 @@ sonarqube { property "sonar.sourceEncoding", "utf-8" property "sonar.sources", "src/main" - property "sonar.java.binaries", "${project.buildDir}/intermediates/javac/debug/classes" + property "sonar.java.binaries", layout.buildDirectory.dir("intermediates/javac/debug/compileDebugJavaWithJavac/classes").get().asFile.absolutePath property "sonar.tests", ["src/test/", "../test/src/androidTest/"] - property "sonar.android.lint.report", "${project.buildDir}/outputs/lint-results.xml" - property "sonar.java.spotbugs.reportPaths", ["${project.buildDir}/reports/spotbugs/debug.xml", "${project.buildDir}/reports/spotbugs/release.xml"] - property "sonar.java.pmd.reportPaths", "${project.buildDir}/reports/pmd/pmd.xml" - property "sonar.java.checkstyle.reportPaths", "${project.buildDir}/reports/checkstyle/checkstyle.xml" - property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/jacocoUnitTestReport.xml" + property "sonar.android.lint.report", layout.buildDirectory.dir("outputs/lint-results.xml").get().asFile.absolutePath + property "sonar.java.spotbugs.reportPaths", ["debug", "release"].collect { layout.buildDirectory.dir("reports/spotbugs/${it}.xml").get().asFile.absolutePath } + property "sonar.java.pmd.reportPaths", layout.buildDirectory.dir("reports/pmd/pmd.xml").get().asFile.absolutePath + property "sonar.java.checkstyle.reportPaths", layout.buildDirectory.dir("reports/checkstyle/checkstyle.xml").get().asFile.absolutePath + property "sonar.coverage.jacoco.xmlReportPaths", layout.buildDirectory.dir("reports/jacoco/jacocoUnitTestReport.xml").get().asFile.absolutePath } } -project.tasks["sonarqube"].dependsOn "check" +project.tasks.named("sonarqube").configure { dependsOn "check" } diff --git a/gradle/shared/html-java-gen.gradle b/gradle/shared/html-java-gen.gradle index e96b8ff1..26b81897 100644 --- a/gradle/shared/html-java-gen.gradle +++ b/gradle/shared/html-java-gen.gradle @@ -1,8 +1,8 @@ android.libraryVariants.all { variant -> def packageName = android.namespace def variantName = variant.name.capitalize() - def outputDir = file("${project.buildDir}/generated/source/hcaptcha/${variant.name}/${packageName.replaceAll('\\.', '/')}") - def generateTask = project.task("generate${variantName}JavaClassFromStaticHtml") { + def outputDir = layout.buildDirectory.dir("generated/source/hcaptcha/${variant.name}/${packageName.replaceAll('\\.', '/')}").get().asFile + def generateTask = tasks.register("generate${variantName}JavaClassFromStaticHtml") { group 'Generate' description "Generate HTML java class" @@ -27,5 +27,5 @@ android.libraryVariants.all { variant -> } // preBuild.dependsOn generateTask - variant.registerJavaGeneratingTask(generateTask, outputDir) + variant.registerJavaGeneratingTask(generateTask.get(), outputDir) } \ No newline at end of file diff --git a/gradle/shared/size-check.gradle b/gradle/shared/size-check.gradle index 6bf0817a..0f97beee 100644 --- a/gradle/shared/size-check.gradle +++ b/gradle/shared/size-check.gradle @@ -1,6 +1,6 @@ android.libraryVariants.all { variant -> def variantName = variant.name.capitalize() - project.task("report${variantName}AarSize") { + tasks.register("report${variantName}AarSize") { group 'Help' description "Report ${variant.name} AAR size" dependsOn variant.packageLibraryProvider @@ -12,7 +12,7 @@ android.libraryVariants.all { variant -> } } - project.tasks.findByName("check").dependsOn(project.task("check${variantName}AarSize") { + tasks.register("check${variantName}AarSize") { group 'Verification' description "Checks ${variant.name} AAR size doesn't exceed ${project.ext}Kb" dependsOn variant.packageLibraryProvider @@ -24,5 +24,7 @@ android.libraryVariants.all { variant -> throw new GradleException("${aarPath} size exceeded! ${aarSizeKb}Kbyte > ${MAX_AAR_SIZE_KB}Kbyte") } } - }) + } + + tasks.named("check").configure { dependsOn "check${variantName}AarSize" } } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 84a0b92f..19cfad96 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sdk/build.gradle b/sdk/build.gradle index fa561045..8413d4ce 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -28,11 +28,11 @@ android { // See https://developer.android.com/studio/publish/versioning // versionCode must be integer and be incremented by one for every new update // android system uses this to prevent downgrades - versionCode 44 + versionCode 45 // version number visible to the user // should follow semantic versioning (See https://semver.org) - versionName "4.0.5" + versionName "4.1.0" buildConfigField 'String', 'VERSION_NAME', "\"${defaultConfig.versionName}_${defaultConfig.versionCode}\"" diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptcha.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptcha.java index 8dc4ce02..17ecd64f 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptcha.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptcha.java @@ -103,7 +103,7 @@ void onFailure(final HCaptchaException exception) { .build(); captchaVerifier = new HCaptchaHeadlessWebView(activity, this.config, internalConfig, listener); } else if (this.activity instanceof FragmentActivity) { - captchaVerifier = HCaptchaDialogFragment.newInstance(inputConfig, internalConfig, listener); + captchaVerifier = HCaptchaDialogFragment.newInstance(activity, inputConfig, internalConfig, listener); this.config = inputConfig; } else { throw new IllegalStateException("Visual hCaptcha challenge verification requires FragmentActivity."); diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java index c964ccf7..382d8d0b 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java @@ -4,6 +4,7 @@ import android.animation.AnimatorListenerAdapter; import android.app.Activity; import android.app.Dialog; +import android.content.Context; import android.content.DialogInterface; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; @@ -16,6 +17,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewStub; import android.view.Window; import android.view.WindowManager; import android.widget.LinearLayout; @@ -56,15 +58,19 @@ public final class HCaptchaDialogFragment extends DialogFragment implements IHCa @Nullable private HCaptchaWebViewHelper webViewHelper; - @NonNull + @Nullable private HCaptchaStateListener listener; + @Nullable private LinearLayout loadingContainer; private float defaultDimAmount = 0.6f; private boolean readyForInteraction = false; + @NonNull + private static HCaptchaWebView sPreloadWebView; + /** * Creates a new instance * @@ -72,32 +78,41 @@ public final class HCaptchaDialogFragment extends DialogFragment implements IHCa * @param listener the listener * @return a new instance */ - public static HCaptchaDialogFragment newInstance( - @lombok.NonNull final HCaptchaConfig config, - @lombok.NonNull final HCaptchaInternalConfig internalConfig, - @lombok.NonNull final HCaptchaStateListener listener + static HCaptchaDialogFragment newInstance( + @NonNull Context context, + @NonNull HCaptchaConfig config, + @NonNull HCaptchaInternalConfig internalConfig, + @NonNull HCaptchaStateListener listener ) { HCaptchaLog.d("DialogFragment.newInstance"); final Bundle args = new Bundle(); args.putSerializable(KEY_CONFIG, config); args.putSerializable(KEY_INTERNAL_CONFIG, internalConfig); args.putParcelable(KEY_LISTENER, listener); - final HCaptchaDialogFragment hCaptchaDialogFragment = new HCaptchaDialogFragment(); + final HCaptchaDialogFragment hCaptchaDialogFragment = + new HCaptchaDialogFragment(context, config, internalConfig); hCaptchaDialogFragment.setArguments(args); + hCaptchaDialogFragment.listener = listener; return hCaptchaDialogFragment; } + public HCaptchaDialogFragment() { + super(); + } + + @SuppressWarnings("PMD.AssignmentToNonFinalStatic") + HCaptchaDialogFragment(Context context, HCaptchaConfig config, HCaptchaInternalConfig internalConfig) { + this(); + sPreloadWebView = new HCaptchaWebView(context); //NOSONAR + webViewHelper = new HCaptchaWebViewHelper(new Handler(Looper.getMainLooper()), + context, config, internalConfig, this, sPreloadWebView); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setStyle(STYLE_NO_FRAME, R.style.HCaptchaDialogTheme); - } - @Override - public View onCreateView(@Nullable LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable Bundle savedInstanceState) { - HCaptchaLog.d("DialogFragment.onCreateView"); try { final Bundle args = getArguments(); listener = HCaptchaCompat.getParcelable(args, KEY_LISTENER, HCaptchaStateListener.class); @@ -109,8 +124,37 @@ public View onCreateView(@Nullable LayoutInflater inflater, // According to Firebase Analytics, there are cases where Bundle args are empty. // > 90% of these cases occur on Android 6, and the count of crashes <<< the count of sessions. // This is a quick fix to prevent crashes in production - throw new AssertionError(); + if (listener != null) { + listener.onFailure(new HCaptchaException(HCaptchaError.ERROR)); + } + return; + } + + if (sPreloadWebView == null) { + HCaptchaLog.w("DialogFragment.onCreate: sPreloadWebView cold init"); + sPreloadWebView = new HCaptchaWebView(requireContext()); + } + + if (webViewHelper == null) { + webViewHelper = new HCaptchaWebViewHelper(new Handler(Looper.getMainLooper()), + requireContext(), config, internalConfig, this, sPreloadWebView); + } + } catch (AssertionError | BadParcelableException | ClassCastException e) { + HCaptchaLog.w("DialogFragment.onCreate: cannot create fragment"); + if (listener != null) { + listener.onFailure(new HCaptchaException(HCaptchaError.ERROR)); } + } + } + + @Override + public View onCreateView(@Nullable LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable Bundle savedInstanceState) { + HCaptchaLog.d("DialogFragment.onCreateView"); + try { + final Bundle args = getArguments(); + final HCaptchaConfig config = HCaptchaCompat.getSerializable(args, KEY_CONFIG, HCaptchaConfig.class); if (inflater == null) { throw new InflateException("inflater is null"); @@ -118,14 +162,12 @@ public View onCreateView(@Nullable LayoutInflater inflater, final View rootView = prepareRootView(inflater, container, config); HCaptchaLog.d("DialogFragment.onCreateView inflated"); - final HCaptchaWebView webView = prepareWebView(rootView, config); + prepareWebView(rootView, config); loadingContainer = rootView.findViewById(R.id.loadingContainer); - loadingContainer.setVisibility(Boolean.TRUE.equals(config.getLoading()) ? View.VISIBLE : View.GONE); + loadingContainer.setVisibility(Boolean.TRUE.equals(config.getLoading()) + && !readyForInteraction ? View.VISIBLE : View.GONE); - webViewHelper = new HCaptchaWebViewHelper(new Handler(Looper.getMainLooper()), - requireContext(), config, internalConfig, this, webView); - readyForInteraction = false; return rootView; } catch (AssertionError | BadParcelableException | InflateException | ClassCastException e) { HCaptchaLog.w("Cannot create view. Dismissing dialog..."); @@ -144,7 +186,7 @@ public void onDestroy() { HCaptchaLog.d("DialogFragment.onDestroy"); super.onDestroy(); if (webViewHelper != null) { - webViewHelper.destroy(); + webViewHelper.reset(); } } @@ -174,12 +216,15 @@ public void onCancel(@NonNull DialogInterface dialogInterface) { } private void hideLoadingContainer() { - if (webViewHelper != null && Boolean.TRUE.equals(webViewHelper.getConfig().getLoading())) { + if (loadingContainer != null && webViewHelper != null + && Boolean.TRUE.equals(webViewHelper.getConfig().getLoading())) { loadingContainer.animate().alpha(0.0f).setDuration(200).setListener( new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - loadingContainer.setVisibility(View.GONE); + if (loadingContainer != null) { + loadingContainer.setVisibility(View.GONE); + } } }); } else { @@ -213,7 +258,11 @@ public void onOpen() { readyForInteraction = true; - listener.onOpen(); + if (listener != null) { + listener.onOpen(); + } else { + HCaptchaLog.w("DialogFragment.hideLoadingContainer: dialog was not created, listener == null"); + } } @Override @@ -225,8 +274,10 @@ public void onFailure(@NonNull final HCaptchaException exception) { if (webViewHelper != null) { if (silentRetry) { webViewHelper.resetAndExecute(); - } else { + } else if (listener != null) { listener.onFailure(exception); + } else { + HCaptchaLog.w("Dialog wasn't created, listener == null"); } } } @@ -237,7 +288,12 @@ public void onSuccess(final String token) { if (isAdded()) { dismissAllowingStateLoss(); } - listener.onSuccess(token); + if (listener != null) { + listener.onSuccess(token); + } else { + HCaptchaLog.w("Dialog wasn't created, listener == null"); + } + } @Override @@ -249,13 +305,18 @@ public void startVerification(@NonNull Activity fragmentActivity) { return; } + if (fragmentManager.isStateSaved()) { + HCaptchaLog.w("FragmentActivity is in state saved"); + return; + } + try { show(fragmentManager, HCaptchaDialogFragment.TAG); } catch (IllegalStateException e) { HCaptchaLog.w("DialogFragment.startVerification " + e.getMessage()); // https://stackoverflow.com/q/14262312/902217 // Happens if Fragment is stopped i.e. activity is about to destroy on show call - if (webViewHelper != null) { + if (listener != null) { listener.onFailure(new HCaptchaException(HCaptchaError.ERROR)); } } @@ -295,20 +356,28 @@ private View prepareRootView(@NonNull LayoutInflater inflater, return rootView; } - private HCaptchaWebView prepareWebView(@NonNull View rootView, @NonNull HCaptchaConfig config) { - final HCaptchaWebView webView = rootView.findViewById(R.id.webView); - if (Boolean.FALSE.equals(config.getLoading())) { - webView.setOnTouchListener((view, event) -> { - if (!readyForInteraction && isAdded()) { - final Activity activity = getActivity(); - if (activity != null) { - activity.dispatchTouchEvent(event); - return true; + private void prepareWebView(@NonNull View rootView, @NonNull HCaptchaConfig config) { + final View stub = rootView.findViewById(R.id.webView); + if (stub instanceof ViewStub) { + final ViewGroup parent = (ViewGroup) rootView; + final int index = parent.indexOfChild(stub); + parent.removeView(stub); + if (sPreloadWebView.getParent() != null) { + ((ViewGroup) sPreloadWebView.getParent()).removeView(sPreloadWebView); + } + parent.addView(sPreloadWebView, index); + if (Boolean.FALSE.equals(config.getLoading())) { + sPreloadWebView.setOnTouchListener((view, event) -> { + if (!readyForInteraction && isAdded()) { + final Activity activity = getActivity(); + if (activity != null) { + activity.dispatchTouchEvent(event); + return true; + } } - } - return view.performClick(); - }); + return view.performClick(); + }); + } } - return webView; } } diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java index e86b58c3..ad5c65db 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java @@ -63,6 +63,8 @@ final class HCaptchaWebViewHelper { private void setupWebView(@NonNull final Handler handler) { HCaptchaLog.d("WebViewHelper.setupWebView"); + webView.setId(R.id.webView); + final HCaptchaJSInterface jsInterface = new HCaptchaJSInterface(handler, config, captchaVerifier); final HCaptchaDebugInfo debugInfo = new HCaptchaDebugInfo(context); final WebSettings settings = webView.getSettings(); diff --git a/sdk/src/main/res/layout/hcaptcha_fragment.xml b/sdk/src/main/res/layout/hcaptcha_fragment.xml index 3ab75bc7..e33e81e2 100644 --- a/sdk/src/main/res/layout/hcaptcha_fragment.xml +++ b/sdk/src/main/res/layout/hcaptcha_fragment.xml @@ -3,7 +3,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - diff --git a/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaTest.java b/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaTest.java index d65212b6..ab1fad5c 100644 --- a/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaTest.java +++ b/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaTest.java @@ -15,6 +15,7 @@ import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; +import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Bundle; @@ -78,6 +79,7 @@ public void init() { dialogFragmentMock = mockStatic(HCaptchaDialogFragment.class); dialogFragmentMock .when(() -> HCaptchaDialogFragment.newInstance( + any(Context.class), any(HCaptchaConfig.class), any(HCaptchaInternalConfig.class), any(HCaptchaStateListener.class))) @@ -99,6 +101,7 @@ public void test_fragment_creation_via_activity() { assertNotNull(HCaptcha.getClient(fragmentActivity).setup(HCaptchaConfigTest.MOCK_SITE_KEY)); dialogFragmentMock.verify(() -> HCaptchaDialogFragment.newInstance( + any(Context.class), hCaptchaConfigCaptor.capture(), any(HCaptchaInternalConfig.class), any(HCaptchaStateListener.class))); @@ -139,6 +142,7 @@ public void test_verify_with_hcaptcha_passes_site_key_as_config() { dialogFragmentMock.verify(() -> HCaptchaDialogFragment.newInstance( + any(Context.class), any(HCaptchaConfig.class), any(HCaptchaInternalConfig.class), any(HCaptchaStateListener.class)), never()); @@ -147,6 +151,7 @@ public void test_verify_with_hcaptcha_passes_site_key_as_config() { dialogFragmentMock.verify(() -> HCaptchaDialogFragment.newInstance( + any(Context.class), hCaptchaConfigCaptor.capture(), any(HCaptchaInternalConfig.class), any(HCaptchaStateListener.class))); @@ -174,6 +179,7 @@ public void test_verify_site_key_arg_has_priority_over_metadata() throws Excepti verify(packageManager, never()).getApplicationInfo(any(String.class), anyInt()); dialogFragmentMock.verify(() -> HCaptchaDialogFragment.newInstance( + any(Context.class), hCaptchaConfigCaptor.capture(), any(HCaptchaInternalConfig.class), any(HCaptchaStateListener.class))); @@ -191,6 +197,7 @@ public void test_setup_config() throws Exception { verify(packageManager, never()).getApplicationInfo(any(String.class), anyInt()); dialogFragmentMock.verify(() -> HCaptchaDialogFragment.newInstance( + any(Context.class), hCaptchaConfigCaptor.capture(), any(HCaptchaInternalConfig.class), any(HCaptchaStateListener.class))); @@ -213,6 +220,7 @@ public void test_verify_config_has_priority_over_setup_config() throws Exception verify(packageManager, never()).getApplicationInfo(any(String.class), anyInt()); dialogFragmentMock.verify(() -> HCaptchaDialogFragment.newInstance( + any(Context.class), hCaptchaConfigCaptor.capture(), any(HCaptchaInternalConfig.class), any(HCaptchaStateListener.class)), times(2)); @@ -230,6 +238,7 @@ public void test_verify_site_key_has_priority_over_setup_config() throws Excepti verify(packageManager, never()).getApplicationInfo(any(String.class), anyInt()); dialogFragmentMock.verify(() -> HCaptchaDialogFragment.newInstance( + any(Context.class), hCaptchaConfigCaptor.capture(), any(HCaptchaInternalConfig.class), any(HCaptchaStateListener.class)), times(2)); diff --git a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java index c4ef51c4..859b0936 100644 --- a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java +++ b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java @@ -344,7 +344,7 @@ public void testVerifyOnStoppedFragmentNoException() throws InterruptedException final CountDownLatch latch = new CountDownLatch(1); try (ActivityScenario scenario = ActivityScenario.launch(TestActivity.class)) { scenario.moveToState(Lifecycle.State.CREATED).onActivity(activity -> { - HCaptchaDialogFragment.newInstance(config, internalConfig, + HCaptchaDialogFragment.newInstance(activity, config, internalConfig, new HCaptchaStateTestAdapter()) .startVerification(activity); latch.countDown(); diff --git a/test/src/main/AndroidManifest.xml b/test/src/main/AndroidManifest.xml index a9f4be7f..462865ba 100644 --- a/test/src/main/AndroidManifest.xml +++ b/test/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - + + android:exported="true" + tools:ignore="MissingClass">