diff --git a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchLauncher.java b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchLauncher.java index 5b30d350..26974a26 100644 --- a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchLauncher.java +++ b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchLauncher.java @@ -1,70 +1,62 @@ package com.braintreepayments.api; -import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; -import android.net.Uri; import androidx.activity.ComponentActivity; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.ActivityResultRegistry; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.LifecycleOwner; -import java.util.UUID; -class BrowserSwitchLauncher { +public class BrowserSwitchLauncher { - private static final String BROWSER_SWITCH_RESULT = "com.braintreepayments.api.BrowserSwitch.RESULT"; + private BrowserSwitchLauncherCallback callback; + private InternalBrowserSwitchLauncher internalBrowserSwitchLauncher; + private BrowserSwitchResult browserSwitchResult; - ActivityResultLauncher activityLauncher; + private Intent handledIntent; - BrowserSwitchLauncher(@NonNull ComponentActivity activity, - @NonNull BrowserSwitchLauncherCallback callback) { - this(activity.getActivityResultRegistry(), activity, callback); + public BrowserSwitchLauncher(Fragment fragment, BrowserSwitchLauncherCallback callback) { + this.callback = callback; + this.internalBrowserSwitchLauncher = new InternalBrowserSwitchLauncher(fragment, browserSwitchResult -> { + this.browserSwitchResult = browserSwitchResult; + }); } - BrowserSwitchLauncher(@NonNull Fragment fragment, - @NonNull BrowserSwitchLauncherCallback callback) { - this(fragment.getActivity().getActivityResultRegistry(), fragment.getViewLifecycleOwner(), - callback); + public BrowserSwitchLauncher(ComponentActivity activity, BrowserSwitchLauncherCallback callback) { + this.callback = callback; + this.internalBrowserSwitchLauncher = new InternalBrowserSwitchLauncher(activity, browserSwitchResult -> { + // In the case of a true user cancellation (user closes the browser without completing), + // this result will be delivered before handleReturnToAppFromBrowser is invoked in + // onResume. After a successful result is delivered, the browser activity will be closed + // in the background, delivering a false cancel result. + this.browserSwitchResult = browserSwitchResult; + }); } - BrowserSwitchLauncher(ActivityResultRegistry registry, LifecycleOwner lifecycleOwner, - BrowserSwitchLauncherCallback callback) { - activityLauncher = registry.register(BROWSER_SWITCH_RESULT, lifecycleOwner, - new BrowserSwitchActivityResultContract(), callback::onResult); + public void launch(BrowserSwitchOptions browserSwitchOptions) throws BrowserSwitchException { + internalBrowserSwitchLauncher.launch(browserSwitchOptions); } - void launch(BrowserSwitchOptions browserSwitchOptions) throws BrowserSwitchException { - try { - activityLauncher.launch(browserSwitchOptions); - } catch (ActivityNotFoundException e) { - throw new BrowserSwitchException(e.getMessage()); - } - } - - void clearActiveRequests(@NonNull Context context) { - BrowserSwitchPersistentStore.getInstance().clearActiveRequest(context.getApplicationContext()); - } - - @Nullable - public BrowserSwitchResult parseResult(@NonNull Context context, int requestCode, @Nullable Intent intent) { + public void handleReturnToAppFromBrowser(@NonNull Context context, int requestId, @NonNull Intent intent) { BrowserSwitchResult result = null; - if (intent != null && intent.getData() != null) { - BrowserSwitchRequest request = - BrowserSwitchPersistentStore.getInstance().getActiveRequest(context.getApplicationContext()); - if (request != null && request.getRequestCode() == requestCode) { - Uri deepLinkUrl = intent.getData(); - if (request.matchesDeepLinkUrlScheme(deepLinkUrl)) { - result = new BrowserSwitchResult(BrowserSwitchStatus.SUCCESS, request, deepLinkUrl); - } - } + // If browser-switch is re-launched from the same activity, a false previous success result + // may be re-delivered even if the user cancels this time, check if we have already handled + // the intent before delivering a success result again + if (!intent.equals(handledIntent)) { + // Handle successful deep link back to app first, to avoid false cancel result (above) + result = internalBrowserSwitchLauncher.parseResult(context, requestId, intent); + } + if (result == null) { + // If the app was not successfully resumed via deep link, check if there was a true + // cancel result + result = this.browserSwitchResult; + } + if (result != null) { + callback.onResult(result); + internalBrowserSwitchLauncher.clearActiveRequests(context); + this.browserSwitchResult = null; + handledIntent = intent; } - return result; } } diff --git a/browser-switch/src/main/java/com/braintreepayments/api/InternalBrowserSwitchLauncher.java b/browser-switch/src/main/java/com/braintreepayments/api/InternalBrowserSwitchLauncher.java new file mode 100644 index 00000000..175197b2 --- /dev/null +++ b/browser-switch/src/main/java/com/braintreepayments/api/InternalBrowserSwitchLauncher.java @@ -0,0 +1,66 @@ +package com.braintreepayments.api; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import androidx.activity.ComponentActivity; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.ActivityResultRegistry; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.LifecycleOwner; + +class InternalBrowserSwitchLauncher { + + private static final String BROWSER_SWITCH_RESULT = "com.braintreepayments.api.BrowserSwitch.RESULT"; + + ActivityResultLauncher activityLauncher; + + InternalBrowserSwitchLauncher(@NonNull ComponentActivity activity, + @NonNull BrowserSwitchLauncherCallback callback) { + this(activity.getActivityResultRegistry(), activity, callback); + } + + InternalBrowserSwitchLauncher(@NonNull Fragment fragment, + @NonNull BrowserSwitchLauncherCallback callback) { + this(fragment.getActivity().getActivityResultRegistry(), fragment.getViewLifecycleOwner(), + callback); + } + + InternalBrowserSwitchLauncher(ActivityResultRegistry registry, LifecycleOwner lifecycleOwner, + BrowserSwitchLauncherCallback callback) { + activityLauncher = registry.register(BROWSER_SWITCH_RESULT, lifecycleOwner, + new BrowserSwitchActivityResultContract(), callback::onResult); + } + + void launch(BrowserSwitchOptions browserSwitchOptions) throws BrowserSwitchException { + try { + activityLauncher.launch(browserSwitchOptions); + } catch (ActivityNotFoundException e) { + throw new BrowserSwitchException(e.getMessage()); + } + } + + void clearActiveRequests(@NonNull Context context) { + BrowserSwitchPersistentStore.getInstance().clearActiveRequest(context.getApplicationContext()); + } + + @Nullable + public BrowserSwitchResult parseResult(@NonNull Context context, int requestCode, @Nullable Intent intent) { + BrowserSwitchResult result = null; + if (intent != null && intent.getData() != null) { + BrowserSwitchRequest request = + BrowserSwitchPersistentStore.getInstance().getActiveRequest(context.getApplicationContext()); + if (request != null && request.getRequestCode() == requestCode) { + Uri deepLinkUrl = intent.getData(); + if (request.matchesDeepLinkUrlScheme(deepLinkUrl)) { + result = new BrowserSwitchResult(BrowserSwitchStatus.SUCCESS, request, deepLinkUrl); + } + } + } + return result; + } +} diff --git a/browser-switch/src/main/java/com/braintreepayments/api/PublicBrowserSwitchLauncher.java b/browser-switch/src/main/java/com/braintreepayments/api/PublicBrowserSwitchLauncher.java deleted file mode 100644 index e747baf6..00000000 --- a/browser-switch/src/main/java/com/braintreepayments/api/PublicBrowserSwitchLauncher.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.braintreepayments.api; - -import android.content.Context; -import android.content.Intent; - -import androidx.activity.ComponentActivity; -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; - - -public class PublicBrowserSwitchLauncher { - - private BrowserSwitchLauncherCallback callback; - private BrowserSwitchLauncher browserSwitchLauncher; - private BrowserSwitchResult browserSwitchResult; - - public PublicBrowserSwitchLauncher(Fragment fragment, BrowserSwitchLauncherCallback callback) { - this.callback = callback; - this.browserSwitchLauncher = new BrowserSwitchLauncher(fragment, browserSwitchResult -> { - this.browserSwitchResult = browserSwitchResult; - }); - } - - public PublicBrowserSwitchLauncher(ComponentActivity activity, BrowserSwitchLauncherCallback callback) { - this.callback = callback; - this.browserSwitchLauncher = new BrowserSwitchLauncher(activity, browserSwitchResult -> { - this.browserSwitchResult = browserSwitchResult; - }); - } - - public void launch(BrowserSwitchOptions browserSwitchOptions) throws BrowserSwitchException { - browserSwitchLauncher.launch(browserSwitchOptions); - } - - public void handleReturnToAppFromBrowser(@NonNull Context context, int requestId, @NonNull Intent intent) { - BrowserSwitchResult result = browserSwitchLauncher.parseResult(context, requestId, intent); - if (result == null) { - result = this.browserSwitchResult; - } - if (result != null) { - callback.onResult(result); - browserSwitchLauncher.clearActiveRequests(context); - this.browserSwitchResult = null; - } - } -} diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 54474b9f..207079b2 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -7,9 +7,23 @@ android:icon="@mipmap/ic_launcher" android:label="Browser Switch Demo" android:theme="@style/AppTheme"> + + + + + + + + + + + + android:exported="true"> diff --git a/demo/src/main/java/com/braintreepayments/api/demo/LauncherActivity.kt b/demo/src/main/java/com/braintreepayments/api/demo/LauncherActivity.kt index fbcb1d7b..2921c239 100644 --- a/demo/src/main/java/com/braintreepayments/api/demo/LauncherActivity.kt +++ b/demo/src/main/java/com/braintreepayments/api/demo/LauncherActivity.kt @@ -8,20 +8,21 @@ import androidx.appcompat.app.AppCompatActivity import com.braintreepayments.api.BrowserSwitchOptions import com.braintreepayments.api.BrowserSwitchResult import com.braintreepayments.api.BrowserSwitchStatus -import com.braintreepayments.api.PublicBrowserSwitchLauncher +import com.braintreepayments.api.BrowserSwitchLauncher import org.json.JSONException import org.json.JSONObject class LauncherActivity : AppCompatActivity() { - private lateinit var launcher: PublicBrowserSwitchLauncher + private lateinit var launcher: BrowserSwitchLauncher override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_launcher) - launcher = PublicBrowserSwitchLauncher(this) { browserSwitchResult -> - onBrowserSwitchResult(browserSwitchResult) - } + launcher = + BrowserSwitchLauncher(this) { browserSwitchResult -> + onBrowserSwitchResult(browserSwitchResult) + } val launchButton = findViewById