Skip to content
Merged
Comment thread
OS-ruimoreiramendes marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.*;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* The Browser class implements Custom Chrome Tabs. See
Expand Down Expand Up @@ -43,7 +46,11 @@ interface BrowserEventListener {
private CustomTabsClient customTabsClient;
private CustomTabsSession browserSession;
private boolean isInitialLoad = false;
private EventGroup group;
private final AtomicBoolean browserFinishedFired = new AtomicBoolean(false);

@Nullable
private ActivityResultLauncher<Intent> customTabLauncher;

private CustomTabsServiceConnection connection = new CustomTabsServiceConnection() {
@Override
public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
Expand All @@ -61,7 +68,6 @@ public void onServiceDisconnected(ComponentName name) {}
*/
public Browser(@NonNull Context context) {
this.context = context;
this.group = new EventGroup(this::handleGroupCompletion);
}

/**
Expand All @@ -81,6 +87,16 @@ public BrowserEventListener getBrowserEventListenerListener() {
return browserEventListener;
}

/**
* Provide the ActivityResultLauncher used to open the Custom Tab. When
* set, the Custom Tab is launched via this launcher so that
* {@link #notifyBrowserFinished()} can be triggered from the launcher's
* result callback (only when the tab activity actually terminates).
*/
public void setCustomTabLauncher(@Nullable ActivityResultLauncher<Intent> launcher) {
this.customTabLauncher = launcher;
}

/**
* Open the browser to the specified URL.
* @param url
Expand Down Expand Up @@ -108,8 +124,13 @@ public void open(Uri url, @Nullable Integer toolbarColor) {
tabsIntent.intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse(Intent.URI_ANDROID_APP_SCHEME + "//" + context.getPackageName()));

isInitialLoad = true;
group.reset();
tabsIntent.launchUrl(context, url);
browserFinishedFired.set(false);
if (customTabLauncher != null) {
tabsIntent.intent.setData(url);
customTabLauncher.launch(tabsIntent.intent);
} else {
tabsIntent.launchUrl(context, url);
}
}

/**
Expand All @@ -120,17 +141,14 @@ public boolean bindService() {
if (null == customTabPackageName) {
customTabPackageName = FALLBACK_CUSTOM_TAB_PACKAGE_NAME;
}
boolean result = CustomTabsClient.bindCustomTabsService(context, customTabPackageName, connection);
group.leave();
return result;
return CustomTabsClient.bindCustomTabsService(context, customTabPackageName, connection);
}

/**
* Unbind the custom tabs service, required to be called in the `onPause` lifecycle event.
*/
public void unbindService() {
context.unbindService(connection);
group.enter();
}

private void handledNavigationEvent(int navigationEvent) {
Expand All @@ -143,20 +161,14 @@ private void handledNavigationEvent(int navigationEvent) {
isInitialLoad = false;
}
break;
case CustomTabsCallback.TAB_HIDDEN:
group.leave();
break;
case CustomTabsCallback.TAB_SHOWN:
group.enter();
break;
}
}

private void handleGroupCompletion() {
// events such as TAB_HIDDEN and onPause can occur for multiple reasons and in
// different sequences so there is no single point to fire this. so we rely on the
// event group to track when it is safe to assume that the browser is done.
if (browserEventListener != null) {
public void notifyBrowserFinished() {
// Guarded so the finished event is delivered at most once per open()
// session, even when multiple signals (ActivityResult, onMinimized)
// fire for the same browser instance.
if (browserFinishedFired.compareAndSet(false, true) && browserEventListener != null) {
browserEventListener.onBrowserEvent(BROWSER_FINISHED);
}
}
Expand All @@ -174,6 +186,18 @@ private CustomTabsSession getCustomTabsSession() {
public void onNavigationEvent(int navigationEvent, Bundle extras) {
handledNavigationEvent(navigationEvent);
}

@Override
public void onMinimized(@NonNull Bundle extras) {
// On Android <14 the ActivityResult callback is not
// reliably delivered when the Custom Tab is dismissed from PiP,
// so we notify listeners as the Custom Tab enters PiP as a
// compromise. Skipped on 14+ where the ActivityResult
// callback handles it correctly.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
notifyBrowserFinished();
}
}
}
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,69 @@
package com.capacitorjs.plugins.browser;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.activity.ComponentActivity;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;

public class BrowserControllerActivity extends Activity {
public class BrowserControllerActivity extends ComponentActivity {

private boolean isCustomTabsOpen = false;
private ActivityResultLauncher<Intent> customTabLauncher;
private Browser implementation;
Comment thread
OS-pedrogustavobilro marked this conversation as resolved.
private boolean isFirstResume = true;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
isCustomTabsOpen = false;

customTabLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), (result) -> {
if (implementation != null) {
implementation.notifyBrowserFinished();
}
finish();
});

if (BrowserPlugin.browserControllerListener != null) {
BrowserPlugin.browserControllerListener.onControllerReady(this);
}
}

@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (intent.hasExtra("close")) {
protected void onResume() {
super.onResume();
// On Android <14 the ActivityResult callback is not reliably delivered
// when the Custom Tab is dismissed from PiP, leaving this launcher
// stranded on top of the task and freezing the app UI. Finishing on
// the second resume (fires when control returns from the Custom Tab) releases
// the task. Skipped on 14+ where the ActivityResult callback handles it.
if (isFirstResume) {
isFirstResume = false;
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
finish();
}
}

@Override
protected void onResume() {
super.onResume();
if (isCustomTabsOpen) {
isCustomTabsOpen = false;
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (intent.hasExtra("close")) {
finish();
} else {
isCustomTabsOpen = true;
}
}

public void open(Browser implementation, Uri url, Integer toolbarColor) {
this.implementation = implementation;
implementation.setCustomTabLauncher(customTabLauncher);
implementation.open(url, toolbarColor);
}

@Override
protected void onDestroy() {
super.onDestroy();
isCustomTabsOpen = false;
implementation = null;
BrowserPlugin.setBrowserControllerListener(null);
}
}

This file was deleted.

Loading