Commit 55ff13ac authored by Pavel Shmakov's avatar Pavel Shmakov Committed by Commit Bot

Web Share Target for TWA 2/2

This CL builds the implementation of Web Share Target for TWAs
on top of the previous one (http://crrev.com/c/1715374):

- Adds TwaIntentHandlingStrategy for TWA-specific logic of intent
handling. It sends the intent to TwaSharingController to check
whether it's a sharing intent.

- Adds TwaSharingController that parses share
target json, constructs and launches GET requests, and delegates
handling POST requests to WebApkPostShareTargetNavigator.

- Adds unit tests for share target parsing, and a few integration
tests for the whole sharing process.


Bug: 985331
Change-Id: Ifa98e46a5e5c681a30442aecfbd6df57a60d9e39
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1724686
Commit-Queue: Pavel Shmakov <pshmakov@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Reviewed-by: default avatarPeter Conn <peconn@chromium.org>
Reviewed-by: default avatarPeter Kotwicz <pkotwicz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#688554}
parent 77aba032
......@@ -665,7 +665,7 @@ deps = {
},
'src/third_party/android_sdk/androidx_browser/src': {
'url': Var('chromium_git') + '/external/gob/android/platform/frameworks/support/browser.git' + '@' + 'aeeea8bd0a6703bc4a148e9bcd6998553def74ab',
'url': Var('chromium_git') + '/external/gob/android/platform/frameworks/support/browser.git' + '@' + 'fe843d13cd587d066c3bb3e5c636089a4a05056b',
'condition': 'checkout_android',
},
......
......@@ -159,10 +159,12 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/TrustedWebActivityPermissionStore.java",
"java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/TrustedWebActivityCoordinator.java",
"java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/TrustedWebActivityModel.java",
"java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/TwaIntentHandlingStrategy.java",
"java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/controller/ClientAppDataRecorder.java",
"java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/controller/TrustedWebActivityDisclosureController.java",
"java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/controller/TrustedWebActivityOpenTimeRecorder.java",
"java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/controller/TrustedWebActivityVerifier.java",
"java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/sharing/TwaSharingController.java",
"java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/splashscreen/SplashImageHolder.java",
"java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/splashscreen/TwaSplashController.java",
"java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/view/TrustedWebActivityDisclosureView.java",
......
......@@ -64,7 +64,9 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkTest.java",
"javatests/src/org/chromium/chrome/browser/browserservices/OriginVerifierTest.java",
"javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityClientTest.java",
"javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityShareTargetTest.java",
"javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityTest.java",
"javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityTestUtil.java",
"javatests/src/org/chromium/chrome/browser/browserservices/permissiondelegation/TrustedWebActivityPermissionsTest.java",
"javatests/src/org/chromium/chrome/browser/browserservices/permissiondelegation/TrustedWebActivityPreferencesUiTest.java",
"javatests/src/org/chromium/chrome/browser/browsing_data/BrowsingDataRemoverIntegrationTest.java",
......
......@@ -1167,6 +1167,7 @@ by a child template that "extends" this file.
<category android:name="androidx.browser.trusted.category.TrustedWebActivitySplashScreensV1"/>
<category android:name="androidx.browser.customtabs.category.NavBarColorCustomization"/>
<category android:name="androidx.browser.customtabs.category.ColorSchemeCustomization"/>
<category android:name="androidx.browser.trusted.category.WebShareTargetV2"/>
</intent-filter>
</service>
<service android:name="androidx.browser.customtabs.PostMessageService" />
......
......@@ -1961,6 +1961,7 @@
<category android:name="androidx.browser.trusted.category.TrustedWebActivities"/>
<category
android:name="androidx.browser.trusted.category.TrustedWebActivitySplashScreensV1"/>
<category android:name="androidx.browser.trusted.category.WebShareTargetV2"/>
</intent-filter>
</service>
<service
......
......@@ -134,7 +134,7 @@ public class TrustedWebActivityClient {
}
int id = service.getSmallIconId();
if (id == TrustedWebActivityService.NO_ID) {
if (id == TrustedWebActivityService.SMALL_ICON_NOT_SET) {
recordFallback(FALLBACK_ICON_NOT_PROVIDED);
return;
}
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.browserservices.trustedwebactivityui;
import org.chromium.chrome.browser.browserservices.trustedwebactivityui.sharing.TwaSharingController;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
import org.chromium.chrome.browser.customtabs.content.CustomTabIntentHandlingStrategy;
import org.chromium.chrome.browser.customtabs.content.DefaultCustomTabIntentHandlingStrategy;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import javax.inject.Inject;
/**
* TWA-specific implementation of {@link CustomTabIntentHandlingStrategy}.
* Currently adds Web Share Target capabilities on top of the Custom Tabs intent handling.
*/
@ActivityScope
public class TwaIntentHandlingStrategy implements CustomTabIntentHandlingStrategy {
private final DefaultCustomTabIntentHandlingStrategy mDefaultStrategy;
private final TwaSharingController mSharingController;
@Inject
public TwaIntentHandlingStrategy(DefaultCustomTabIntentHandlingStrategy defaultStrategy,
TwaSharingController sharingController) {
mDefaultStrategy = defaultStrategy;
mSharingController = sharingController;
}
@Override
public void handleInitialIntent(CustomTabIntentDataProvider intentDataProvider) {
handleIntent(intentDataProvider);
}
@Override
public void handleNewIntent(CustomTabIntentDataProvider intentDataProvider) {
// TODO(pshmakov): we can have a significant delay here in case of POST sharing.
// Allow showing splash screen, if it's provided in the intent.
handleIntent(intentDataProvider);
}
private void handleIntent(CustomTabIntentDataProvider intentDataProvider) {
mSharingController.deliverToShareTarget(intentDataProvider).then((delivered) -> {
if (!delivered) {
mDefaultStrategy.handleInitialIntent(intentDataProvider);
}
});
}
}
......@@ -11,6 +11,7 @@ import android.support.annotation.Nullable;
import org.chromium.base.ContextUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.Promise;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.browserservices.Origin;
......@@ -103,7 +104,7 @@ public class TrustedWebActivityVerifier implements NativeInitObserver, Destroyab
assert false : "Shouldn't observe navigation when TWAs are disabled";
return;
}
verify(new Origin(navigation.getUrl()));
verifyVisitedOrigin(new Origin(navigation.getUrl()));
}
};
......@@ -114,7 +115,7 @@ public class TrustedWebActivityVerifier implements NativeInitObserver, Destroyab
// When a link with target="_blank" is followed and the user navigates back, we
// don't get the onDidFinishNavigation event (because the original page wasn't
// navigated away from, it was only ever hidden). https://crbug.com/942088
verify(new Origin(tab.getUrl()));
verifyVisitedOrigin(new Origin(tab.getUrl()));
}
};
......@@ -184,7 +185,7 @@ public class TrustedWebActivityVerifier implements NativeInitObserver, Destroyab
}
collectTrustedOrigins(initialOrigin);
verify(initialOrigin);
verifyVisitedOrigin(initialOrigin);
// This doesn't belong here, but doesn't deserve a separate class. Do extract it if more
// PostMessage-related code appears.
......@@ -205,15 +206,32 @@ public class TrustedWebActivityVerifier implements NativeInitObserver, Destroyab
}
}
/** Returns whether the given |url| is on an Origin that the package has been verified for. */
/**
* Returns whether the given |url| is on an Origin that the package has been previously
* verified for.
*/
public boolean isPageOnVerifiedOrigin(String url) {
return mOriginVerifier.wasPreviouslyVerified(new Origin(url));
}
/**
* Perform verification for the given origin.
* Verifies an arbitrary url.
* Returns a {@link Promise<Boolean>} with boolean telling whether verification succeeded.
*/
public Promise<Boolean> verifyOrigin(String url) {
if (mOriginVerifier.wasPreviouslyVerified(new Origin(url))) {
return Promise.fulfilled(true);
}
Promise<Boolean> promise = new Promise<>();
mOriginVerifier.start((packageName, origin, verified, online) -> promise.fulfill(verified),
new Origin(url));
return promise;
}
/**
* Perform verification for the origin the user is currently on.
*/
private void verify(Origin origin) {
private void verifyVisitedOrigin(Origin origin) {
if (mOriginsToVerify.contains(origin)) {
// Do verification bypassing the cache.
updateState(origin, VerificationStatus.PENDING);
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.browserservices.trustedwebactivityui.sharing;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Pair;
import org.chromium.base.Promise;
import org.chromium.chrome.browser.browserservices.trustedwebactivityui.controller.TrustedWebActivityVerifier;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityNavigationController;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabProvider;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.webapps.WebApkInfo;
import org.chromium.chrome.browser.webapps.WebApkPostShareTargetNavigator;
import java.util.ArrayList;
import java.util.Locale;
import javax.inject.Inject;
import androidx.browser.trusted.sharing.ShareData;
import androidx.browser.trusted.sharing.ShareTarget;
/**
* Handles sharing intents coming to Trusted Web Activities.
*/
@ActivityScope
public class TwaSharingController {
private final CustomTabActivityTabProvider mTabProvider;
private final CustomTabActivityNavigationController mNavigationController;
private final WebApkPostShareTargetNavigator mPostNavigator;
private final TrustedWebActivityVerifier mVerifier;
@Inject
public TwaSharingController(CustomTabActivityTabProvider tabProvider,
CustomTabActivityNavigationController navigationController,
WebApkPostShareTargetNavigator postNavigator,
TrustedWebActivityVerifier verifier) {
mTabProvider = tabProvider;
mNavigationController = navigationController;
mPostNavigator = postNavigator;
mVerifier = verifier;
}
/**
* Checks whether the incoming intent (represented by a {@link CustomTabIntentDataProvider})
* is a sharing intent and attempts to perform the sharing.
*
* Returns a {@link Promise<Boolean>} with a boolean telling whether sharing was successful.
*/
public Promise<Boolean> deliverToShareTarget(CustomTabIntentDataProvider intentDataProvider) {
ShareData shareData = intentDataProvider.getShareData();
ShareTarget shareTarget = intentDataProvider.getShareTarget();
if (shareTarget == null || shareData == null) {
return Promise.fulfilled(false);
}
return mVerifier.verifyOrigin(shareTarget.action).then(
(Promise.Function<Boolean, Boolean>) (verified) -> {
if (!verified) {
return false;
}
WebApkInfo.ShareTarget target = toShareTargetInternal(shareTarget);
if (target.isShareMethodPost()) {
return sendPost(shareData, target);
}
mNavigationController.navigate(computeStartUrlForGETShareTarget(shareData, target));
return true;
});
}
/**
* Converts to internal format.
* TODO(pshmakov): pull WebApkInfo.ShareTarget out of WebApkInfo and rename to
* ShareTargetInternal. Also, replace WebApkInfo.ShareData with ShareData from TWA API.
*/
private WebApkInfo.ShareTarget toShareTargetInternal(ShareTarget shareTarget) {
ShareTarget.Params params = shareTarget.params;
String action = shareTarget.action;
String paramTitle = params.title;
String paramText = params.text;
String paramUrl = ""; // Not supported on Android
String method = shareTarget.method;
boolean isPost = method != null && "POST".equals(method.toUpperCase(Locale.ENGLISH));
String encodingType = shareTarget.encodingType;
boolean isMultipart = encodingType != null &&
"multipart/form-data".equals(encodingType.toLowerCase(Locale.ENGLISH));
int numFiles = params.files == null ? 0 : params.files.size();
String[] filesArray = new String[numFiles];
String[][] acceptsArray = new String[numFiles][];
for (int i = 0; i < numFiles; i++) {
ShareTarget.FileFormField file = params.files.get(i);
filesArray[i] = file.name;
acceptsArray[i] = file.acceptedTypes.toArray(new String[file.acceptedTypes.size()]);
}
return new WebApkInfo.ShareTarget(action, paramTitle, paramText, paramUrl, isPost,
isMultipart, filesArray, acceptsArray);
}
private boolean sendPost(ShareData shareData, WebApkInfo.ShareTarget target) {
WebApkInfo.ShareData webApkData = new WebApkInfo.ShareData();
if (shareData.uris != null) {
webApkData.files = new ArrayList<>(shareData.uris);
}
webApkData.subject = shareData.title;
webApkData.text = shareData.text;
Tab tab = mTabProvider.getTab();
if (tab == null) {
assert false : "Null tab when sharing";
return false;
}
return mPostNavigator.navigateIfPostShareTarget(target.getAction(), target, webApkData,
tab.getWebContents());
}
// Copy of HostBrowserLauncherParams#computeStartUrlForGETShareTarget().
// Since the latter is in the WebAPK client code, we can't reuse it.
private static String computeStartUrlForGETShareTarget(
ShareData data, WebApkInfo.ShareTarget target) {
// These can be null, they are checked downstream.
ArrayList<Pair<String, String>> entryList = new ArrayList<>();
entryList.add(new Pair<>(target.getParamTitle(), data.title));
entryList.add(new Pair<>(target.getParamText(), data.text));
return createGETWebShareTargetUriString(target.getAction(), entryList);
}
private static String createGETWebShareTargetUriString(
String action, ArrayList<Pair<String, String>> entryList) {
Uri.Builder queryBuilder = new Uri.Builder();
for (Pair<String, String> nameValue : entryList) {
if (!TextUtils.isEmpty(nameValue.first) && !TextUtils.isEmpty(nameValue.second)) {
// Uri.Builder does URL escaping.
queryBuilder.appendQueryParameter(nameValue.first, nameValue.second);
}
}
Uri shareUri = Uri.parse(action);
Uri.Builder builder = shareUri.buildUpon();
// Uri.Builder uses %20 rather than + for spaces, the spec requires +.
String queryString = queryBuilder.build().toString();
if (TextUtils.isEmpty(queryString)) {
return action;
}
builder.encodedQuery(queryString.replace("%20", "+").substring(1));
return builder.build().toString();
}
}
......@@ -6,6 +6,8 @@ package org.chromium.chrome.browser.browserservices.trustedwebactivityui.splashs
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static androidx.browser.trusted.TrustedWebActivityIntentBuilder.EXTRA_SPLASH_SCREEN_PARAMS;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
......@@ -33,7 +35,8 @@ import org.chromium.ui.base.ActivityWindowAndroid;
import javax.inject.Inject;
import androidx.browser.customtabs.TrustedWebUtils;
import androidx.browser.customtabs.TrustedWebUtils.SplashScreenParamKey;
import androidx.browser.trusted.TrustedWebActivityIntentBuilder;
import androidx.browser.trusted.splashscreens.SplashScreenParamKey;
/**
* Orchestrates the flow of showing and removing splash screens for apps based on Trusted Web
......@@ -174,8 +177,7 @@ public class TwaSplashController
}
private Bundle getSplashScreenParamsFromIntent() {
return mIntentDataProvider.getIntent().getBundleExtra(
TrustedWebUtils.EXTRA_SPLASH_SCREEN_PARAMS);
return mIntentDataProvider.getIntent().getBundleExtra(EXTRA_SPLASH_SCREEN_PARAMS);
}
/**
......@@ -184,9 +186,8 @@ public class TwaSplashController
public static boolean intentIsForTwaWithSplashScreen(Intent intent) {
boolean isTrustedWebActivity = IntentUtils.safeGetBooleanExtra(
intent, TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, false);
boolean requestsSplashScreen = IntentUtils.safeGetParcelableExtra(
intent, TrustedWebUtils.EXTRA_SPLASH_SCREEN_PARAMS)
!= null;
boolean requestsSplashScreen =
IntentUtils.safeGetParcelableExtra(intent, EXTRA_SPLASH_SCREEN_PARAMS) != null;
return isTrustedWebActivity && requestsSplashScreen;
}
......@@ -200,7 +201,7 @@ public class TwaSplashController
if (!intentIsForTwaWithSplashScreen(intent)) return false;
Bundle params = IntentUtils.safeGetBundleExtra(
intent, TrustedWebUtils.EXTRA_SPLASH_SCREEN_PARAMS);
intent, TrustedWebActivityIntentBuilder.EXTRA_SPLASH_SCREEN_PARAMS);
boolean shownInClient = IntentUtils.safeGetBoolean(params, KEY_SHOWN_IN_CLIENT, true);
// shownInClient is "true" by default for the following reasons:
// - For compatibility with older clients which don't use this bundle key.
......
......@@ -403,7 +403,7 @@ public class CustomTabActivity extends ChromeActivity<CustomTabActivityComponent
}
@Override
protected void onNewIntent(Intent intent) {
public void onNewIntent(Intent intent) {
Intent originalIntent = getIntent();
super.onNewIntent(intent);
// Currently we can't handle arbitrary updates of intent parameters, so make sure
......
......@@ -56,6 +56,9 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsSessionToken;
import androidx.browser.customtabs.TrustedWebUtils;
import androidx.browser.trusted.TrustedWebActivityIntentBuilder;
import androidx.browser.trusted.sharing.ShareData;
import androidx.browser.trusted.sharing.ShareTarget;
/**
* A model class that parses the incoming intent for Custom Tabs specific customization data.
......@@ -323,7 +326,7 @@ public class CustomTabIntentDataProvider extends BrowserSessionDataProvider {
mIsTrustedWebActivity = IntentUtils.safeGetBooleanExtra(
intent, TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, false);
mTrustedWebActivityAdditionalOrigins = IntentUtils.safeGetStringArrayListExtra(intent,
TrustedWebUtils.EXTRA_ADDITIONAL_TRUSTED_ORIGINS);
TrustedWebActivityIntentBuilder.EXTRA_ADDITIONAL_TRUSTED_ORIGINS);
mTitleVisibilityState = IntentUtils.safeGetIntExtra(
intent, CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE, CustomTabsIntent.NO_TITLE);
mShowShareItem = IntentUtils.safeGetBooleanExtra(intent,
......@@ -846,7 +849,7 @@ public class CustomTabIntentDataProvider extends BrowserSessionDataProvider {
/**
* @return Whether the Custom Tab should attempt to display a Trusted Web Activity.
*/
boolean isTrustedWebActivity() {
public boolean isTrustedWebActivity() {
return mIsTrustedWebActivity;
}
......@@ -950,4 +953,38 @@ public class CustomTabIntentDataProvider extends BrowserSessionDataProvider {
ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_TARGET_TRANSLATE_LANGUAGE);
return isEnabled ? mTranslateLanguage : null;
}
/**
* Returns {@link ShareTarget} describing the share target, or null if the intent is not about
* sharing.
*/
@Nullable
public ShareTarget getShareTarget() {
Bundle bundle = IntentUtils.safeGetBundleExtra(mIntent,
TrustedWebActivityIntentBuilder.EXTRA_SHARE_TARGET);
if (bundle == null) return null;
try {
return ShareTarget.fromBundle(bundle);
} catch (Throwable e) {
// Catch unparcelling errors.
return null;
}
}
/**
* Returns {@link ShareData} describing the data to be shared, or null if the intent is not
* about sharing.
*/
@Nullable
public ShareData getShareData() {
Bundle bundle = IntentUtils.safeGetParcelableExtra(mIntent,
TrustedWebActivityIntentBuilder.EXTRA_SHARE_DATA);
if (bundle == null) return null;
try {
return ShareData.fromBundle(bundle);
} catch (Throwable e) {
// Catch unparcelling errors.
return null;
}
}
}
......@@ -55,7 +55,7 @@ public class CustomTabsClientFileProcessor {
return false;
}
switch (purpose) {
case CustomTabsService.FILE_PURPOSE_TWA_SPLASH_IMAGE:
case CustomTabsService.FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE:
return receiveTwaSplashImage(session, uri);
}
Log.w(TAG, "Unknown FilePurpose " + purpose);
......
......@@ -5,14 +5,18 @@
package org.chromium.chrome.browser.customtabs.dependency_injection;
import org.chromium.chrome.browser.browserservices.ClientAppDataRegister;
import org.chromium.chrome.browser.browserservices.trustedwebactivityui.TwaIntentHandlingStrategy;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
import org.chromium.chrome.browser.customtabs.CustomTabNightModeStateController;
import org.chromium.chrome.browser.customtabs.content.CustomTabIntentHandler.IntentIgnoringCriterion;
import org.chromium.chrome.browser.customtabs.content.CustomTabIntentHandlingStrategy;
import org.chromium.chrome.browser.customtabs.content.DefaultCustomTabIntentHandlingStrategy;
import org.chromium.chrome.browser.webapps.WebApkPostShareTargetNavigator;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
import dagger.Reusable;
/**
* Module for custom tab specific bindings.
......@@ -48,12 +52,19 @@ public class CustomTabActivityModule {
@Provides
public CustomTabIntentHandlingStrategy provideIntentHandler(
DefaultCustomTabIntentHandlingStrategy defaultHandler) {
return defaultHandler;
Lazy<DefaultCustomTabIntentHandlingStrategy> defaultHandler,
Lazy<TwaIntentHandlingStrategy> twaHandler) {
return mIntentDataProvider.isTrustedWebActivity() ? twaHandler.get() : defaultHandler.get();
}
@Provides
public IntentIgnoringCriterion provideIntentIgnoringCriterion() {
return mIntentIgnoringCriterion;
}
@Provides
@Reusable
public WebApkPostShareTargetNavigator providePostShareTargetNavigator() {
return new WebApkPostShareTargetNavigator();
}
}
......@@ -7,6 +7,7 @@ package org.chromium.chrome.browser.webapps;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import android.text.TextUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.library_loader.LibraryLoader;
......@@ -182,8 +183,14 @@ public class WebApkActivity extends WebappActivity {
@Override
protected boolean loadUrlIfPostShareTarget(WebappInfo webappInfo) {
WebApkInfo webApkInfo = (WebApkInfo) webappInfo;
return WebApkPostShareTargetNavigator.navigateIfPostShareTarget(
webApkInfo, getActivityTab().getWebContents());
WebApkInfo.ShareData shareData = webApkInfo.shareData();
if (shareData == null || !TextUtils.equals(shareData.shareActivityClassName,
webApkInfo.shareTargetActivityName())) {
return false;
}
return new WebApkPostShareTargetNavigator().navigateIfPostShareTarget(
webApkInfo.url(), webApkInfo.shareTarget(), shareData,
getActivityTab().getWebContents());
}
@Override
......
......@@ -49,7 +49,7 @@ import java.util.Map;
*/
public class WebApkInfo extends WebappInfo {
// A class that stores share information from share intent.
protected static class ShareData {
public static class ShareData {
public String subject;
public String text;
public ArrayList<Uri> files;
......
......@@ -4,17 +4,19 @@
package org.chromium.chrome.browser.webapps;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.content_public.browser.WebContents;
/**
* Perform navigation for share target with POST request.
*/
public class WebApkPostShareTargetNavigator {
public static boolean navigateIfPostShareTarget(
WebApkInfo webApkInfo, WebContents webContents) {
public boolean navigateIfPostShareTarget(
String url,
WebApkInfo.ShareTarget target,
WebApkInfo.ShareData data, WebContents webContents) {
WebApkShareTargetUtil.PostData postData =
WebApkShareTargetUtil.computePostData(webApkInfo.shareTargetActivityName(),
webApkInfo.shareTarget(), webApkInfo.shareData());
WebApkShareTargetUtil.computePostData(target, data);
if (postData == null) {
return false;
}
......@@ -24,14 +26,18 @@ public class WebApkPostShareTargetNavigator {
isValueFileUris[i] = postData.isValueFileUri.get(i);
}
nativeLoadViewForShareTargetPost(postData.isMultipartEncoding,
postData.names.toArray(new String[0]), postData.values.toArray(new String[0]),
isValueFileUris, postData.filenames.toArray(new String[0]),
postData.types.toArray(new String[0]), webApkInfo.url(), webContents);
WebApkPostShareTargetNavigatorJni.get().nativeLoadViewForShareTargetPost(
postData.isMultipartEncoding, postData.names.toArray(new String[0]),
postData.values.toArray(new String[0]), isValueFileUris,
postData.filenames.toArray(new String[0]), postData.types.toArray(new String[0]),
url, webContents);
return true;
}
private static native void nativeLoadViewForShareTargetPost(boolean isMultipartEncoding,
String[] names, String[] values, boolean[] isValueFileUris, String[] filenames,
String[] types, String startUrl, WebContents webContents);
@NativeMethods
public interface Natives {
void nativeLoadViewForShareTargetPost(boolean isMultipartEncoding,
String[] names, String[] values, boolean[] isValueFileUris, String[] filenames,
String[] types, String startUrl, WebContents webContents);
}
}
......@@ -150,7 +150,7 @@ public class WebApkShareTargetUtil {
for (Uri fileUri : shareFiles) {
String fileType, fileName;
try (StrictModeContext strictModeContextUnused = StrictModeContext.allowDiskReads()) {
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
fileType = getFileTypeFromContentUri(fileUri);
fileName = getFileNameFromContentUri(fileUri);
}
......@@ -193,10 +193,9 @@ public class WebApkShareTargetUtil {
}
}
protected static PostData computePostData(String shareTargetActivityName,
protected static PostData computePostData(
WebApkInfo.ShareTarget shareTarget, WebApkInfo.ShareData shareData) {
if (shareTarget == null || !shareTarget.isShareMethodPost() || shareData == null
|| !shareData.shareActivityClassName.equals(shareTargetActivityName)) {
if (shareTarget == null || !shareTarget.isShareMethodPost() || shareData == null) {
return null;
}
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.browserservices;
import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.createSession;
import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.createTrustedWebActivityIntent;
import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.spoofVerification;
import android.content.Intent;
import android.support.test.filters.MediumTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ContextUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.webapps.WebApkPostShareTargetNavigator;
import org.chromium.chrome.browser.webapps.WebApkPostShareTargetNavigatorJni;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.net.test.EmbeddedTestServerRule;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import androidx.browser.trusted.TrustedWebActivityIntentBuilder;
import androidx.browser.trusted.sharing.ShareData;
import androidx.browser.trusted.sharing.ShareTarget;
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class TrustedWebActivityShareTargetTest {
// We are not actually navigating to POST target, so ok not to use test pages here.
private static final ShareTarget POST_SHARE_TARGET =
new ShareTarget("https://pwa.rocks/share.html", "POST", null,
new ShareTarget.Params("received_title", "received_text", null));
private static final ShareTarget UNVERIFIED_ORIGIN_POST_SHARE_TARGET =
new ShareTarget("https://random.website/share.html", "POST", null,
new ShareTarget.Params("received_title", "received_text", null));
@Rule
public CustomTabActivityTestRule mCustomTabActivityTestRule = new CustomTabActivityTestRule();
@Rule
public EmbeddedTestServerRule mEmbeddedTestServerRule = new EmbeddedTestServerRule();
@Rule
public JniMocker mJniMocker = new JniMocker();
private static final String TEST_PAGE = "/chrome/test/data/android/google.html";
private static final String SHARE_TEST_PAGE = "/chrome/test/data/android/about.html";
private static final String PACKAGE_NAME =
ContextUtils.getApplicationContext().getPackageName();
private final MockPostNavigatorNatives mPostNavigatorNatives = new MockPostNavigatorNatives();
private final CallbackHelper mPostNavigatorCallback = new CallbackHelper();
private Intent mIntent;
private ShareTarget mGetShareTarget;
// Expected URL when using mGetShareTarget as input.
private String mExpectedGetRequestUrl;
@Before
public void setUp() throws Exception {
mJniMocker.mock(WebApkPostShareTargetNavigatorJni.TEST_HOOKS, mPostNavigatorNatives);
LibraryLoader.getInstance().ensureInitialized(LibraryProcessType.PROCESS_BROWSER);
mEmbeddedTestServerRule.setServerUsesHttps(true);
String testPage = mEmbeddedTestServerRule.getServer().getURL(TEST_PAGE);
String shareTestPage = mEmbeddedTestServerRule.getServer().getURL(SHARE_TEST_PAGE);
mGetShareTarget = new ShareTarget(shareTestPage, "GET", null,
new ShareTarget.Params("received_title", "received_text", null));
mExpectedGetRequestUrl = shareTestPage
+ "?received_title=test_title&received_text=test_text";
spoofVerification(PACKAGE_NAME, testPage);
spoofVerification(PACKAGE_NAME, "https://pwa.rocks");
mIntent = createTrustedWebActivityIntent(testPage);
createSession(mIntent, PACKAGE_NAME);
}
@Test
@MediumTest
public void sharesDataWithGet_FromInitialIntent() throws Exception {
putShareData(mIntent, mGetShareTarget);
mCustomTabActivityTestRule.startCustomTabActivityWithIntent(mIntent);
assertGetRequestUrl(mExpectedGetRequestUrl);
}
@Test
@MediumTest
public void sharesDataWithPost_FromInitialIntent() throws Exception {
putShareData(mIntent, POST_SHARE_TARGET);
mCustomTabActivityTestRule.startCustomTabActivityWithIntent(mIntent);
assertPostNavigatorCalled();
}
@Test
@MediumTest
public void sharesDataWithPost_FromNewIntent() throws Exception {
mCustomTabActivityTestRule.startCustomTabActivityWithIntent(mIntent);
putShareData(mIntent, POST_SHARE_TARGET);
deliverNewIntent(mIntent);
assertPostNavigatorCalled();
}
@Test
@MediumTest
public void sharesDataWithGet_FromNewIntent() throws Exception {
mCustomTabActivityTestRule.startCustomTabActivityWithIntent(mIntent);
putShareData(mIntent, mGetShareTarget);
deliverNewIntent(mIntent);
assertGetRequestUrl(mExpectedGetRequestUrl);
}
@Test(expected = TimeoutException.class)
@MediumTest
public void doesntShareWithUnverifiedOrigin() throws Exception {
putShareData(mIntent, UNVERIFIED_ORIGIN_POST_SHARE_TARGET);
mCustomTabActivityTestRule.startCustomTabActivityWithIntent(mIntent);
mPostNavigatorCallback.waitForCallback(0, 1, 1000, TimeUnit.MILLISECONDS);
}
private void putShareData(Intent intent, ShareTarget shareTarget) {
ShareData shareData = new ShareData("test_title", "test_text", Collections.emptyList());
intent.putExtra(TrustedWebActivityIntentBuilder.EXTRA_SHARE_DATA, shareData.toBundle());
intent.putExtra(TrustedWebActivityIntentBuilder.EXTRA_SHARE_TARGET, shareTarget.toBundle());
}
private void assertGetRequestUrl(final String expectedGetRequestUrl)
throws InterruptedException {
// startCustomTabActivityWithIntent waits for native, so the tab must be present already.
Tab tab = mCustomTabActivityTestRule.getActivity().getActivityTab();
ChromeTabUtils.waitForTabPageLoaded(tab, expectedGetRequestUrl);
}
private void assertPostNavigatorCalled() throws InterruptedException, TimeoutException {
// Constructing POST requests is unit-tested elsewhere.
// Here we only care that the request reaches the native code.
mPostNavigatorCallback.waitForCallback(0);
}
private void deliverNewIntent(Intent intent) {
// Delivering intents to existing CustomTabActivity in tests is error-prone and out of scope
// of these tests. Thus calling onNewIntent directly.
TestThreadUtils.runOnUiThreadBlocking(
() -> mCustomTabActivityTestRule.getActivity().onNewIntent(intent));
}
private class MockPostNavigatorNatives implements WebApkPostShareTargetNavigator.Natives {
@Override
public void nativeLoadViewForShareTargetPost(boolean isMultipartEncoding, String[] names,
String[] values, boolean[] isValueFileUris, String[] filenames, String[] types,
String startUrl, WebContents webContents) {
mPostNavigatorCallback.notifyCalled();
}
}
}
......@@ -7,11 +7,14 @@ package org.chromium.chrome.browser.browserservices;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.createSession;
import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.createTrustedWebActivityIntent;
import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.isTrustedWebActivity;
import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.spoofVerification;
import android.content.Intent;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
......@@ -24,18 +27,11 @@ import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.customtabs.CustomTabsTestUtils;
import org.chromium.chrome.browser.tab.TabBrowserControlsState;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.net.test.ServerCertificate;
import org.chromium.net.test.EmbeddedTestServerRule;
import java.util.concurrent.TimeoutException;
import androidx.browser.customtabs.CustomTabsService;
import androidx.browser.customtabs.CustomTabsSessionToken;
import androidx.browser.customtabs.TrustedWebUtils;
/**
......@@ -48,12 +44,13 @@ public class TrustedWebActivityTest {
// TODO(peconn): Add test for navigating away from the trusted origin.
@Rule
public CustomTabActivityTestRule mCustomTabActivityTestRule = new CustomTabActivityTestRule();
@Rule
public EmbeddedTestServerRule mEmbeddedTestServerRule = new EmbeddedTestServerRule();
private static final String TEST_PAGE = "/chrome/test/data/android/google.html";
private static final String PACKAGE_NAME =
ContextUtils.getApplicationContext().getPackageName();
private EmbeddedTestServer mTestServer;
private String mTestPage;
@Before
......@@ -61,48 +58,8 @@ public class TrustedWebActivityTest {
// Native needs to be initialized to start the test server.
LibraryLoader.getInstance().ensureInitialized(LibraryProcessType.PROCESS_BROWSER);
// TWAs only work with HTTPS.
mTestServer = EmbeddedTestServer.createAndStartHTTPSServer(
InstrumentationRegistry.getInstrumentation().getContext(),
ServerCertificate.CERT_OK);
mTestPage = mTestServer.getURL(TEST_PAGE);
}
/** Creates an Intent that will launch a Custom Tab to the given |url|. */
private static Intent createTrustedWebActivityIntent(String url) {
Intent intent = CustomTabsTestUtils.createMinimalCustomTabIntent(
InstrumentationRegistry.getTargetContext(), url);
intent.putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true);
return intent;
}
/** Caches a successful verification for the given |packageName| and |url|. */
private static void spoofVerification(String packageName, String url) {
TestThreadUtils.runOnUiThreadBlocking(
() -> OriginVerifier.addVerificationOverride(packageName, new Origin(url),
CustomTabsService.RELATION_HANDLE_ALL_URLS));
}
/** Creates a Custom Tabs Session from the Intent, specifying the |packageName|. */
private static void createSession(Intent intent, String packageName)
throws TimeoutException, InterruptedException {
CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
connection.newSession(token);
connection.overridePackageNameForSessionForTesting(token, packageName);
}
private boolean isTrustedWebActivity() {
// A key part of the Trusted Web Activity UI is the lack of browser controls.
return !TestThreadUtils.runOnUiThreadBlockingNoException(
() -> TabBrowserControlsState
.get(mCustomTabActivityTestRule.getActivity().getActivityTab())
.canShow());
}
@After
public void tearDown() throws TimeoutException {
mTestServer.stopAndDestroyServer();
mEmbeddedTestServerRule.setServerUsesHttps(true); // TWAs only work with HTTPS.
mTestPage = mEmbeddedTestServerRule.getServer().getURL(TEST_PAGE);
}
@Test
......@@ -114,7 +71,7 @@ public class TrustedWebActivityTest {
mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
assertTrue(isTrustedWebActivity());
assertTrue(isTrustedWebActivity(mCustomTabActivityTestRule.getActivity()));
}
@Test
......@@ -128,7 +85,7 @@ public class TrustedWebActivityTest {
mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
assertFalse(isTrustedWebActivity());
assertFalse(isTrustedWebActivity(mCustomTabActivityTestRule.getActivity()));
}
@Test
......@@ -139,6 +96,6 @@ public class TrustedWebActivityTest {
mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
assertFalse(isTrustedWebActivity());
assertFalse(isTrustedWebActivity(mCustomTabActivityTestRule.getActivity()));
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.browserservices;
import android.content.Intent;
import android.support.test.InstrumentationRegistry;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.customtabs.CustomTabsTestUtils;
import org.chromium.chrome.browser.tab.TabBrowserControlsState;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import java.util.concurrent.TimeoutException;
import androidx.browser.customtabs.CustomTabsService;
import androidx.browser.customtabs.CustomTabsSessionToken;
import androidx.browser.customtabs.TrustedWebUtils;
public class TrustedWebActivityTestUtil {
/** Creates an Intent that will launch a Custom Tab to the given |url|. */
public static Intent createTrustedWebActivityIntent(String url) {
Intent intent = CustomTabsTestUtils.createMinimalCustomTabIntent(
InstrumentationRegistry.getTargetContext(), url);
intent.putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true);
return intent;
}
/** Caches a successful verification for the given |packageName| and |url|. */
public static void spoofVerification(String packageName, String url) {
TestThreadUtils.runOnUiThreadBlocking(
() -> OriginVerifier.addVerificationOverride(packageName, new Origin(url),
CustomTabsService.RELATION_HANDLE_ALL_URLS));
}
/** Creates a Custom Tabs Session from the Intent, specifying the |packageName|. */
public static void createSession(Intent intent, String packageName)
throws TimeoutException, InterruptedException {
CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
connection.newSession(token);
connection.overridePackageNameForSessionForTesting(token, packageName);
}
/** Checks if given instance of {@link CustomTabActivity} is a Trusted Web Activity. */
public static boolean isTrustedWebActivity(CustomTabActivity activity) {
// A key part of the Trusted Web Activity UI is the lack of browser controls.
return !TestThreadUtils.runOnUiThreadBlockingNoException(
() -> TabBrowserControlsState
.get(activity.getActivityTab())
.canShow());
}
}
......@@ -155,7 +155,7 @@ void NavigateShareTargetPost(
}
} // namespace webapk
void JNI_WebApkPostShareTargetNavigator_LoadViewForShareTargetPost(
static void JNI_WebApkPostShareTargetNavigator_NativeLoadViewForShareTargetPost(
JNIEnv* env,
const jboolean java_is_multipart_encoding,
const JavaParamRef<jobjectArray>& java_names,
......
......@@ -25,7 +25,7 @@ public class TestTrustedWebActivityService extends TrustedWebActivityService {
}
@Override
protected boolean notifyNotificationWithChannel(String platformTag, int platformId,
public boolean notifyNotificationWithChannel(String platformTag, int platformId,
Notification notification, String channelName) {
MessengerService.sMessageHandler
.recordNotifyNotification(platformTag, platformId, channelName);
......@@ -33,12 +33,12 @@ public class TestTrustedWebActivityService extends TrustedWebActivityService {
}
@Override
protected void cancelNotification(String platformTag, int platformId) {
public void cancelNotification(String platformTag, int platformId) {
MessengerService.sMessageHandler.recordCancelNotification(platformTag, platformId);
}
@Override
protected int getSmallIconId() {
public int getSmallIconId() {
MessengerService.sMessageHandler.recordGetSmallIconId();
return SMALL_ICON_ID;
}
......
......@@ -23,7 +23,11 @@ android_library("androidx_browser_java") {
"./src/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityServiceWrapper.java",
"./src/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionManager.java",
"./src/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityService.java",
"./src/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityBuilder.java",
"./src/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityIntent.java",
"./src/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityIntentBuilder.java",
"./src/browser/src/main/java/androidx/browser/trusted/sharing/ShareData.java",
"./src/browser/src/main/java/androidx/browser/trusted/sharing/ShareTarget.java",
"./src/browser/src/main/java/androidx/browser/trusted/splashscreens/SplashScreenParamKey.java",
]
deps = [
"//third_party/android_deps:android_support_v7_appcompat_java",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment