Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,47 @@ This SDK does not transfer any information over the network. Web browsing inform
stored if the WebView fallback is enabled. The permission to read the location can be managed
via the usual Android settings.

## Using Shortcuts in Trusted Web Activities

When implementing shortcuts (e.g. from `shortcuts.xml`) in a Trusted Web Activity (TWA) application, launching the TWA through `LauncherActivity` on Android Desktop (such as ChromeOS) can result in unresponsive windows due to window manager interactions with translucent activities.

To prevent this issue, you should use the dedicated `ShortcutTrampolineActivity` for all your app's shortcut intents.

### 1. Create a `shortcuts.xml` resource

Create `res/xml/shortcuts.xml` and target `ShortcutTrampolineActivity` as the `targetClass`, passing the shortcut target URL in the `android:data` field:

```xml
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="twa_shortcut"
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:shortcutShortLabel="@string/shortcut_label">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="YOUR_PACKAGE_NAME"
android:targetClass="com.google.androidbrowserhelper.trusted.ShortcutTrampolineActivity"
android:data="https://your-twa-domain.com/shortcut-target-url" />
</shortcut>
</shortcuts>
```

### 2. Reference the shortcuts in your Launcher Activity

In your `AndroidManifest.xml`, reference `shortcuts.xml` within the `<activity>` tag of your main launcher activity:

```xml
<activity android:name=".MyLauncherActivity" ...>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
...
</activity>
```

`ShortcutTrampolineActivity` runs with `Theme.NoDisplay` and will process the shortcut launch securely by validating the URL against your configured TWA domains, routing the launch asynchronously using the application context, and closing itself instantly before any window transitions are impacted.

## Source Code Headers

Every file containing source code must include copyright and license
Expand Down
7 changes: 7 additions & 0 deletions androidbrowserhelper/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,11 @@
<data android:scheme="https" />
</intent>
</queries>

<application>
<activity
android:name="com.google.androidbrowserhelper.trusted.ShortcutTrampolineActivity"
android:theme="@android:style/Theme.NoDisplay"
android:exported="true"/>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -209,39 +209,16 @@ protected void launchTwa() {
return;
}

CustomTabColorSchemeParams defaultColorScheme = new CustomTabColorSchemeParams.Builder()
.setNavigationBarColor(getColorCompat(mMetadata.navigationBarColorId))
.setNavigationBarDividerColor(getColorCompat(mMetadata.navigationBarDividerColorId))
.setToolbarColor(getColorCompat(mMetadata.statusBarColorId))
.build();
CustomTabColorSchemeParams darkModeColorScheme = new CustomTabColorSchemeParams.Builder()
.setToolbarColor(getColorCompat(mMetadata.statusBarColorDarkId))
.setNavigationBarColor(getColorCompat(mMetadata.navigationBarColorDarkId))
.setNavigationBarDividerColor(
getColorCompat(mMetadata.navigationBarDividerColorDarkId))
.build();

Uri launchUrl = getLaunchingUrl();
TrustedWebActivityIntentBuilder twaBuilder =
new TrustedWebActivityIntentBuilder(launchUrl)
.setDefaultColorSchemeParams(defaultColorScheme)
.setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM)
.setColorSchemeParams(
CustomTabsIntent.COLOR_SCHEME_DARK, darkModeColorScheme)
.setDisplayMode(getDisplayMode())
.setDisplayOverrideList(mMetadata.displayOverrideList)
.setScreenOrientation(mMetadata.screenOrientation)
.setLaunchHandlerClientMode(mMetadata.launchHandlerClientMode);
TrustedWebActivityIntentBuilder twaBuilder = new TrustedWebActivityIntentBuilder(launchUrl);
mMetadata.configureIntentBuilder(twaBuilder, this);
twaBuilder.setDisplayMode(getDisplayMode());

Uri intentUrl = getUrlForIntent(getIntent());
if (!launchUrl.equals(intentUrl) && intentUrl != null) {
twaBuilder.setOriginalLaunchUrl(intentUrl);
}

if (mMetadata.additionalTrustedOrigins != null) {
twaBuilder.setAdditionalTrustedOrigins(mMetadata.additionalTrustedOrigins);
}

addShareDataIfPresent(twaBuilder);
addFileDataIfPresent(twaBuilder);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,23 @@
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.pm.PackageInfo;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabColorSchemeParams;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.trusted.LaunchHandlerClientMode;
import androidx.browser.trusted.ScreenOrientation;
import androidx.browser.trusted.TrustedWebActivityDisplayMode;
import androidx.browser.trusted.TrustedWebActivityIntentBuilder;
import androidx.core.content.ContextCompat;

import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -342,6 +349,44 @@ private static List<TrustedWebActivityDisplayMode> getDisplayOverride(@NonNull B
return clientMode != null ? clientMode : LaunchHandlerClientMode.AUTO;
}

/**
* Configures the TrustedWebActivityIntentBuilder with TWA metadata parameters.
*/
public void configureIntentBuilder(TrustedWebActivityIntentBuilder builder, Context context) {
CustomTabColorSchemeParams defaultColorScheme = new CustomTabColorSchemeParams.Builder()
.setNavigationBarColor(ContextCompat.getColor(context, navigationBarColorId))
.setNavigationBarDividerColor(ContextCompat.getColor(context, navigationBarDividerColorId))
.setToolbarColor(ContextCompat.getColor(context, statusBarColorId))
.build();
CustomTabColorSchemeParams darkModeColorScheme = new CustomTabColorSchemeParams.Builder()
.setToolbarColor(ContextCompat.getColor(context, statusBarColorDarkId))
.setNavigationBarColor(ContextCompat.getColor(context, navigationBarColorDarkId))
.setNavigationBarDividerColor(
ContextCompat.getColor(context, navigationBarDividerColorDarkId))
.build();

builder.setDefaultColorSchemeParams(defaultColorScheme)
.setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM)
.setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_DARK, darkModeColorScheme)
.setDisplayMode(displayMode)
.setDisplayOverrideList(displayOverrideList)
.setScreenOrientation(screenOrientation)
.setLaunchHandlerClientMode(launchHandlerClientMode);

if (additionalTrustedOrigins != null) {
builder.setAdditionalTrustedOrigins(additionalTrustedOrigins);
}
}

private static boolean tryMergeMetadata(Bundle target, @Nullable ActivityInfo activityInfo) {
if (activityInfo != null && activityInfo.metaData != null
&& activityInfo.metaData.containsKey(METADATA_DEFAULT_URL)) {
target.putAll(activityInfo.metaData);
return true;
}
return false;
}

/**
* Creates LauncherActivityMetadata instance based on metadata of the passed Activity.
*/
Expand Down Expand Up @@ -371,6 +416,40 @@ public static LauncherActivityMetadata parse(Context context) {
// Will only happen if the package provided (the one we are running in) is not
// installed - so should never happen.
}

if (!metaData.containsKey(METADATA_DEFAULT_URL)) {
try {
PackageManager pm = context.getPackageManager();

// 1. Query launcher activities/aliases (handles metadata on alias itself)
Intent queryIntent = new Intent(Intent.ACTION_MAIN);
queryIntent.addCategory(Intent.CATEGORY_LAUNCHER);
queryIntent.setPackage(context.getPackageName());
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
queryIntent, PackageManager.GET_META_DATA);
for (ResolveInfo resolveInfo : resolveInfos) {
if (tryMergeMetadata(metaData, resolveInfo.activityInfo)) {
return new LauncherActivityMetadata(metaData, resources);
}
}

// 2. Fall back to scanning all package activities if METADATA_DEFAULT_URL is
// not inside the metadata.
// (handles metadata on target activity when launcher filter is on an alias)
PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(),
PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
if (packageInfo.activities != null) {
for (ActivityInfo activityInfo : packageInfo.activities) {
if (tryMergeMetadata(metaData, activityInfo)) {
return new LauncherActivityMetadata(metaData, resources);
}
}
}
} catch (PackageManager.NameNotFoundException e) {
// Ignore.
}
}

return new LauncherActivityMetadata(metaData, resources);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationManagerCompat;
import java.util.Locale;

Expand Down Expand Up @@ -55,7 +54,6 @@ public static boolean areNotificationsEnabled(Context context, String channelNam
/**
* Checks if high-priority notifications are configured in the manifest metadata.
*/
@VisibleForTesting
static boolean shouldUseHighPriorityNotifications(Context context) {
if (!(context instanceof Service)) return false;
try {
Expand Down Expand Up @@ -137,7 +135,6 @@ static void createNotificationChannelAndMaybeDeleteOldOne(Context context, Strin
* Generates a notification channel id from a channel name.
* TODO: Remove this when we can use the method defined in AndroidX instead.
*/
@VisibleForTesting
static String channelNameToId(Context context, String name) {
String baseId = name.toLowerCase(Locale.ROOT).replace(' ', '_');
return shouldUseHighPriorityNotifications(context)
Expand Down
Loading
Loading