У меня есть Android-приложение Trusted Web Activity (TWA), созданное с использованием Bubblewrap. TWA запускается успешно, и служба CustomTabs подключается без проблем.
Однако я не получаю никаких навигационных событий или обратных вызовов канала сообщений при попытке реализовать связь postMessage между собственным кодом Android и веб-контентом.
Моя цель — использовать это сообщение для запуска всплывающего окна с разрешением на уведомление в зависимости от того, зарегистрирован пользователь или нет.
Я следую официальной документации postMessage для TWA, но ожидаемые обратные вызовы не срабатывают.
Каковы могут быть потенциальные причины этого и как правильно установить канал связи postMessage для обработки разрешений на уведомления в TWA?
Среда
- Приложение Android, использующее TWA
- Версия Chrome: 115+ (требуется для TWA postMessage поддержка)
- библиотека androidx.browser: v1.6.0-alpha02+
- Ссылки на цифровые активы (DAL) настроены правильно (проверено запуском TWA без адресной строки)
AndroidManifest.xml:
Код: Выделить всё
Код: Выделить всё
package com.mywebsite.twa;
import android.Manifest;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import androidx.browser.customtabs.CustomTabsSession;
public class LauncherActivity extends com.google.androidbrowserhelper.trusted.LauncherActivity {
private static final String TAG = "MywebsiteTWA";
private static final String MESSAGE_TO_JS = "Hello from Android";
private CustomTabsClient mClient;
private CustomTabsSession mSession;
private boolean mValidated;
private boolean mChannelRequested;
private final CustomTabsCallback mCustomTabsCallback = new CustomTabsCallback() {
@Override
public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) {
String eventName;
switch (navigationEvent) {
case NAVIGATION_STARTED:
eventName = "NAVIGATION_STARTED";
break;
case NAVIGATION_FINISHED:
eventName = "NAVIGATION_FINISHED";
break;
case NAVIGATION_FAILED:
eventName = "NAVIGATION_FAILED";
break;
case NAVIGATION_ABORTED:
eventName = "NAVIGATION_ABORTED";
break;
case TAB_SHOWN:
eventName = "TAB_SHOWN";
break;
case TAB_HIDDEN:
eventName = "TAB_HIDDEN";
break;
default:
eventName = "UNKNOWN(" + navigationEvent + ")";
}
Log.i(TAG, "Navigation event: " + eventName);
if (navigationEvent == TAB_SHOWN && !mChannelRequested) {
Uri launchUrl = Uri.parse(getLaunchingUrl().toString());
Log.i(TAG, "Requesting post message channel for " + launchUrl);
// Using the simpler approach that's been reported to work better
boolean channelRequested = mSession.requestPostMessageChannel(launchUrl);
Log.i(TAG, "Post message channel request result: " + channelRequested);
mChannelRequested = channelRequested;
}
}
@Override
public void onMessageChannelReady(@Nullable Bundle extras) {
Log.i(TAG, "Message channel ready, sending initial message");
int result = mSession.postMessage(MESSAGE_TO_JS, null);
Log.i(TAG, "Initial message send result: " + result);
}
@Override
public void onPostMessage(@NonNull String message, @Nullable Bundle extras) {
Log.i(TAG, "Received message from web: " + message);
// Handle specific messages from web
if ("request_notification_permission".equals(message)) {
requestNotificationPermissionIfNeeded();
}
}
@Override
public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin,
boolean result, @Nullable Bundle extras) {
Log.i(TAG, "Relationship validation result: " + result);
mValidated = result;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate called");
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
initializeCustomTabs();
}
@Override
protected Uri getLaunchingUrl() {
Uri uri = super.getLaunchingUrl();
Log.i(TAG, "Getting launch URL: " + uri.toString());
return uri;
}
protected void requestNotificationPermissionIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
new String[]{Manifest.permission.POST_NOTIFICATIONS},
1001
);
}
}
private void initializeCustomTabs() {
Log.i(TAG, "Initializing CustomTabs");
String packageName = CustomTabsClient.getPackageName(this, null);
Log.i(TAG, "Using browser package: " + packageName);
CustomTabsClient.bindCustomTabsService(this, packageName, new CustomTabsServiceConnection() {
@Override
public void onCustomTabsServiceConnected(@NonNull ComponentName name,
@NonNull CustomTabsClient client) {
Log.i(TAG, "CustomTabs service connected");
mClient = client;
// Warm up must be called before session creation
client.warmup(0L);
Log.i(TAG, "Called warmup on CustomTabsClient");
mSession = mClient.newSession(mCustomTabsCallback);
if (mSession == null) {
Log.e(TAG, "Failed to create new session");
} else {
Log.i(TAG, "CustomTabs session created successfully");
// Pre-load the launch URL
Uri launchUrl = Uri.parse(getLaunchingUrl().toString());
Log.i(TAG, "Pre-loading URL: " + launchUrl);
boolean success = mSession.mayLaunchUrl(launchUrl, null, null);
Log.i(TAG, "URL pre-load result: " + success);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.i(TAG, "CustomTabs service disconnected");
mClient = null;
mSession = null;
mValidated = false;
mChannelRequested = false;
}
});
}
protected CustomTabsSession getSession() {
return mSession;
}
}
Из журналов:
Код: Выделить всё
---------------------------- PROCESS STARTED (27377) for package com.mywebsite.twa ----------------------------
2025-05-13 16:30:44.715 27377-27399 AdrenoGLES-0 com.mywebsite.twa I QUALCOMM build : ee3f1da111, Ie1e5aac4cc
Build Date : 01/11/24
OpenGL ES Shader Compiler Version: EV031.35.01.12
Local Branch :
Remote Branch : refs/tags/AU_LINUX_ANDROID_LA.UM.9.14.11.00.00.571.148
Remote Branch : NONE
Reconstruct Branch : NOTHING
2025-05-13 16:30:44.715 27377-27399 AdrenoGLES-0 com.mywebsite.twa I Build Config : S P 10.0.7 AArch64
2025-05-13 16:30:44.715 27377-27399 AdrenoGLES-0 com.mywebsite.twa I Driver Path : /vendor/lib64/egl/libGLESv2_adreno.so
2025-05-13 16:30:44.724 27377-27399 AdrenoGLES-0 com.mywebsite.twa I PFP: 0x016dc094, ME: 0x00000000
2025-05-13 16:30:44.741 27377-27399 OpenGLRenderer com.mywebsite.twa I setGrContext nullptr-->validptr
2025-05-13 16:30:44.744 27377-27399 OpenGLRenderer com.mywebsite.twa E Unable to match the desired swap behavior.
2025-05-13 16:30:44.758 27377-27377 Compatibil...geReporter com.mywebsite.twa D Compat change id reported: 160794467; UID 10542; state: ENABLED
2025-05-13 16:30:44.761 27377-27377 MywebsiteTWA com.mywebsite.twa I CustomTabs service connected
2025-05-13 16:30:44.763 27377-27377 MywebsiteTWA com.mywebsite.twa I Called warmup on CustomTabsClient
2025-05-13 16:30:44.764 27377-27377 MywebsiteTWA com.mywebsite.twa I CustomTabs session created successfully
2025-05-13 16:30:44.764 27377-27377 TWALauncherActivity com.mywebsite.twa D Using url from Manifest: https://mywebsite.com/index.html
2025-05-13 16:30:44.764 27377-27377 MywebsiteTWA com.mywebsite.twa I Getting launch URL: https://mywebsite.com/index.html
2025-05-13 16:30:44.764 27377-27377 MywebsiteTWA com.mywebsite.twa I Pre-loading URL: https://mywebsite.com/index.html
2025-05-13 16:30:44.765 27377-27377 MywebsiteTWA com.mywebsite.twa I URL pre-load result: true
2025-05-13 16:30:45.098 27377-27377 TwaLauncher com.mywebsite.twa D Launching Trusted Web Activity.
2025-05-13 16:30:46.047 27377-27399 Surface com.mywebsite.twa D Surface::disconnect
2025-05-13 16:30:46.047 27377-27399 BufferQueueProducer com.mywebsite.twa D [VRI[LauncherActivity]#0(BLAST Consumer)0](id:6af100000000,api:1,p:27377,c:27377) disconnect: api 1
2025-05-13 16:30:50.152 27377-27499 ProfileInstaller com.mywebsite.twa D Skipping profile installation for com.mywebsite.twa
- События навигации не регистрируются
- Обратные вызовы канала сообщений не запускаются
- Между собственным кодом и веб-контентом не устанавливается связь
- Первоначально использовалось версия с тремя параметрами
requestPostMessageChannel
mSession.requestPostMessageChannel(sourceOrigin, targetOrigin, new Bundle()) - Переключена на более простую версию с одним параметром, как рекомендовано в
недавних обсуждениях Элемент списка - Настройка проверенных ссылок на цифровые активы (TWA запускается без адресной строки)
- Добавлено правильное ведение журнала для всех методов обратного вызова
- Гарантировано, что веб-контент имеет правильный прослушиватель событий сообщений
- Почему события навигации не запускаются в
CustomTabsCallback? - Что может препятствовать установлению канала сообщений
? - Нужны ли какие-либо дополнительные действия для
правильной инициализации функции postMessage в TWA?
Сам TWA работает корректно — запускается и отображает веб-контент как положено. Проблема кажется специфичной для реализации postMessage. Журналы показывают успешное подключение к службе и предварительную загрузку URL-адреса, но не последующие события навигации или канала сообщений.
Мы будем очень признательны за любую информацию о том, чего может не хватать или что может препятствовать запуску событий обратного вызова.
Официальная документация по адресу: