Commit b9faabca authored by Peter Kotwicz's avatar Peter Kotwicz Committed by Commit Bot

[Android WebAPK] Relaunch WebAPK when the user finishes first run experience

new-style WebAPKs have a different activity architecture than old-style
WebAPKs. new-style WebAPKs stack TransparentSplashWebApkActivity (which
runs in Chrome) on top of the SplashActivity (which runs in the WebAPK).
The SplashActivity keeps running till TransparentSplashWebApkActivity
finishes itself.

In order for tapping the app icon of an already running new-style
WebAPK to activate the already-running WebAPK instead of relaunching
the WebAPK, SplashActivity must be running.

This CL:
- Changes the first run experience to send an intent to relaunch the
WebAPK when the user completes the FRE. Previously,
TransparentSplashWebApkActivity was launched when the user completes
the FRE.
- Changes TransparentSplashWebApkActivity not to be singleTop. If
TransparentSplashWebApkActivity is singleTop,
FirstRunFlowSequencer#launch() triggers
TransparentSplashWebApkActivity#onNewIntent() when it relaunches the
activity with FLAG_ACTIVITY_NEW_TASK

BUG=911420

Change-Id: I1a0d648502e525ec133cfb2ef35c9fb2b5afca53
Reviewed-on: https://chromium-review.googlesource.com/c/1367187
Commit-Queue: Peter Kotwicz <pkotwicz@chromium.org>
Reviewed-by: default avatarDominick Ng <dominickn@chromium.org>
Reviewed-by: default avatarYusuf Ozuysal <yusufo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#616119}
parent ca60fe10
......@@ -707,7 +707,6 @@ by a child template that "extends" this file.
<activity android:name="org.chromium.chrome.browser.webapps.TransparentSplashWebApkActivity"
android:theme="@style/WebappTheme"
android:label="@string/webapp_activity_title"
android:launchMode="singleTop"
android:exported="false"
android:persistableMode="persistNever"
{{ self.supports_video_persistence() }}
......
......@@ -269,8 +269,7 @@ public abstract class FirstRunFlowSequencer {
* @return A generic intent to show the First Run Activity.
* @param context The context.
* @param fromIntent The intent that was used to launch Chrome.
* @param intentToLaunchAfterFreComplete The intent to relaunch Chrome when the user completes
* the FRE.
* @param intentToLaunchAfterFreComplete The intent to launch when the user completes the FRE.
* @param requiresBroadcast Whether the relaunch intent must be broadcasted.
*/
private static Intent createGenericFirstRunIntent(Context context, Intent fromIntent,
......@@ -299,16 +298,20 @@ public abstract class FirstRunFlowSequencer {
* Returns an intent to show the lightweight first run activity.
* @param context The context.
* @param fromIntent The intent that was used to launch Chrome.
* @param intentToLaunchAfterFreComplete The intent to relaunch Chrome when the user completes
* the FRE.
* @param associatedAppName The id of the application associated with the activity
* being launched.
* @param intentToLaunchAfterFreComplete The intent to launch when the user completes the FRE.
* @param requiresBroadcast Whether the relaunch intent must be broadcasted.
*/
private static Intent createLightweightFirstRunIntent(Context context, Intent fromIntent,
Intent intentToLaunchAfterFreComplete, boolean requiresBroadcast) {
String associatedAppName, Intent intentToLaunchAfterFreComplete,
boolean requiresBroadcast) {
Intent intent = new Intent();
intent.setClassName(context, LightweightFirstRunActivity.class.getName());
String appName = WebApkActivity.slowExtractNameFromIntentIfTargetIsWebApk(fromIntent);
intent.putExtra(LightweightFirstRunActivity.EXTRA_ASSOCIATED_APP_NAME, appName);
if (associatedAppName != null) {
intent.putExtra(
LightweightFirstRunActivity.EXTRA_ASSOCIATED_APP_NAME, associatedAppName);
}
addPendingIntent(context, intent, intentToLaunchAfterFreComplete, requiresBroadcast);
return intent;
}
......@@ -317,21 +320,21 @@ public abstract class FirstRunFlowSequencer {
* Adds fromIntent as a PendingIntent to the firstRunIntent. This should be used to add a
* PendingIntent that will be sent when first run is completed.
*
* @param context The context that corresponds to the Intent.
* @param firstRunIntent The intent that will be used to start first run.
* @param fromIntent The intent that was used to launch Chrome.
* @param requiresBroadcast Whether or not the fromIntent must be broadcasted.
* @param context The context that corresponds to the Intent.
* @param firstRunIntent The intent that will be used to start first run.
* @param intentToLaunchAfterFreComplete The intent to launch when the user completes the FRE.
* @param requiresBroadcast Whether or not the fromIntent must be broadcasted.
*/
private static void addPendingIntent(
Context context, Intent firstRunIntent, Intent fromIntent, boolean requiresBroadcast) {
private static void addPendingIntent(Context context, Intent firstRunIntent,
Intent intentToLaunchAfterFreComplete, boolean requiresBroadcast) {
PendingIntent pendingIntent = null;
int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT;
if (requiresBroadcast) {
pendingIntent = PendingIntent.getBroadcast(
context, FIRST_RUN_EXPERIENCE_REQUEST_CODE, fromIntent, pendingIntentFlags);
pendingIntent = PendingIntent.getBroadcast(context, FIRST_RUN_EXPERIENCE_REQUEST_CODE,
intentToLaunchAfterFreComplete, pendingIntentFlags);
} else {
pendingIntent = PendingIntent.getActivity(
context, FIRST_RUN_EXPERIENCE_REQUEST_CODE, fromIntent, pendingIntentFlags);
pendingIntent = PendingIntent.getActivity(context, FIRST_RUN_EXPERIENCE_REQUEST_CODE,
intentToLaunchAfterFreComplete, pendingIntentFlags);
}
firstRunIntent.putExtra(FirstRunActivity.EXTRA_FRE_COMPLETE_LAUNCH_INTENT, pendingIntent);
}
......@@ -341,17 +344,17 @@ public abstract class FirstRunFlowSequencer {
* flags, we first relaunch it to make sure it runs in its own task, then trigger First Run.
*
* @param caller Activity instance that is checking if first run is necessary.
* @param intent Intent used to launch the caller.
* @param fromIntent Intent used to launch the caller.
* @param requiresBroadcast Whether or not the Intent triggers a BroadcastReceiver.
* @param preferLightweightFre Whether to prefer the Lightweight First Run Experience.
* @return Whether startup must be blocked (e.g. via Activity#finish or dropping the Intent).
*/
public static boolean launch(Context caller, Intent intent, boolean requiresBroadcast,
public static boolean launch(Context caller, Intent fromIntent, boolean requiresBroadcast,
boolean preferLightweightFre) {
// Check if the user needs to go through First Run at all.
if (!checkIfFirstRunIsNecessary(caller, intent, preferLightweightFre)) return false;
if (!checkIfFirstRunIsNecessary(caller, fromIntent, preferLightweightFre)) return false;
String intentUrl = IntentHandler.getUrlFromIntent(intent);
String intentUrl = IntentHandler.getUrlFromIntent(fromIntent);
Uri uri = intentUrl != null ? Uri.parse(intentUrl) : null;
if (uri != null && UrlConstants.CONTENT_SCHEME.equals(uri.getScheme())) {
caller.grantUriPermission(
......@@ -359,26 +362,39 @@ public abstract class FirstRunFlowSequencer {
}
Log.d(TAG, "Redirecting user through FRE.");
if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
boolean isVrIntent = VrModuleProvider.getIntentDelegate().isVrIntent(intent);
if ((fromIntent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
boolean isVrIntent = VrModuleProvider.getIntentDelegate().isVrIntent(fromIntent);
boolean isGenericFreActive = checkIsGenericFreActive();
Intent intentToLaunchAfterFreComplete = fromIntent;
String associatedAppNameForLightweightFre = null;
WebApkActivity.FreParams webApkFreParams =
WebApkActivity.slowGenerateFreParamsIfIntentIsForWebApkActivity(fromIntent);
if (webApkFreParams != null) {
intentToLaunchAfterFreComplete =
webApkFreParams.getIntentToLaunchAfterFreComplete();
associatedAppNameForLightweightFre = webApkFreParams.webApkShortName();
}
// Launch the Generic First Run Experience if it was previously active.
Intent freIntent = null;
if (preferLightweightFre && !isGenericFreActive) {
freIntent =
createLightweightFirstRunIntent(caller, intent, intent, requiresBroadcast);
freIntent = createLightweightFirstRunIntent(caller, fromIntent,
associatedAppNameForLightweightFre, intentToLaunchAfterFreComplete,
requiresBroadcast);
} else {
freIntent = createGenericFirstRunIntent(caller, intent, intent, requiresBroadcast);
freIntent = createGenericFirstRunIntent(
caller, fromIntent, intentToLaunchAfterFreComplete, requiresBroadcast);
if (shouldSwitchToTabbedMode(caller)) {
freIntent.setClass(caller, TabbedModeFirstRunActivity.class);
// We switched to TabbedModeFRE. We need to disable animation on the original
// intent, to make transition seamless.
intent = new Intent(intent);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
addPendingIntent(caller, freIntent, intent, requiresBroadcast);
intentToLaunchAfterFreComplete = new Intent(intentToLaunchAfterFreComplete);
intentToLaunchAfterFreComplete.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
addPendingIntent(
caller, freIntent, intentToLaunchAfterFreComplete, requiresBroadcast);
}
}
......@@ -391,7 +407,7 @@ public abstract class FirstRunFlowSequencer {
} else {
// First Run requires that the Intent contains NEW_TASK so that it doesn't sit on top
// of something else.
Intent newIntent = new Intent(intent);
Intent newIntent = new Intent(fromIntent);
newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
IntentUtils.safeStartActivity(caller, newIntent);
}
......
......@@ -39,18 +39,47 @@ public class WebApkActivity extends WebappActivity {
@VisibleForTesting
public static final String STARTUP_UMA_HISTOGRAM_SUFFIX = ".WebApk";
/** WebAPK first run experience parameters. */
public static class FreParams {
private final Intent mIntentToLaunchAfterFreComplete;
private final String mShortName;
public FreParams(Intent intentToLaunchAfterFreComplete, String shortName) {
mIntentToLaunchAfterFreComplete = intentToLaunchAfterFreComplete;
mShortName = shortName;
}
/** Returns the intent launch when the user completes the first run experience. */
public Intent getIntentToLaunchAfterFreComplete() {
return mIntentToLaunchAfterFreComplete;
}
/** Returns the WebAPK's short name. */
public String webApkShortName() {
return mShortName;
}
}
/**
* Tries extracting the WebAPK short name from the passed in intent. Returns null if the intent
* does not launch a WebApkActivity. This method is slow. It makes several PackageManager calls.
* Generates parameters for the WebAPK first run experience for the given intent. Returns null
* if the intent does not launch a WebApkActivity. This method is slow. It makes several
* PackageManager calls.
*/
public static String slowExtractNameFromIntentIfTargetIsWebApk(Intent intent) {
// Check for intents targetted at WebApkActivity and WebApkActivity0-9.
if (!intent.getComponent().getClassName().startsWith(WebApkActivity.class.getName())) {
public static FreParams slowGenerateFreParamsIfIntentIsForWebApkActivity(Intent fromIntent) {
// Check for intents targeted at WebApkActivity, WebApkActivity0-9 and
// TransparentSplashWebApkActivity.
String targetActivityClassName = fromIntent.getComponent().getClassName();
if (!targetActivityClassName.startsWith(WebApkActivity.class.getName())
&& !targetActivityClassName.equals(
TransparentSplashWebApkActivity.class.getName())) {
return null;
}
WebApkInfo info = WebApkInfo.create(intent);
return (info != null) ? info.shortName() : null;
WebApkInfo info = WebApkInfo.create(fromIntent);
return (info != null)
? new FreParams(WebappLauncherActivity.createRelaunchWebApkIntent(fromIntent, info),
info.shortName())
: null;
}
@Override
......
......@@ -54,6 +54,45 @@ public class WebappLauncherActivity extends Activity {
private static final String TAG = "webapps";
/** Creates intent to relaunch WebAPK. */
public static Intent createRelaunchWebApkIntent(Intent sourceIntent, WebApkInfo webApkInfo) {
assert webApkInfo != null;
Intent intent = new Intent(Intent.ACTION_VIEW, webApkInfo.uri());
intent.setPackage(webApkInfo.webApkPackageName());
intent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | ApiCompatibilityUtils.getActivityNewDocumentFlag());
Bundle extras = sourceIntent.getExtras();
if (extras != null) {
intent.putExtras(extras);
}
return intent;
}
/**
* Brings a live WebappActivity back to the foreground if one exists for the given tab ID.
* @param tabId ID of the Tab to bring back to the foreground.
* @return True if a live WebappActivity was found, false otherwise.
*/
public static boolean bringWebappToFront(int tabId) {
if (tabId == Tab.INVALID_TAB_ID) return false;
for (WeakReference<Activity> activityRef : ApplicationStatus.getRunningActivities()) {
Activity activity = activityRef.get();
if (activity == null || !(activity instanceof WebappActivity)) continue;
WebappActivity webappActivity = (WebappActivity) activity;
if (webappActivity.getActivityTab() != null
&& webappActivity.getActivityTab().getId() == tabId) {
Tab tab = webappActivity.getActivityTab();
tab.getTabWebContentsDelegateAndroid().activateContents();
return true;
}
}
return false;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
......@@ -157,15 +196,7 @@ public class WebappLauncherActivity extends Activity {
/** Relaunches WebAPK. */
private static void relaunchWebApk(
Activity launchingActivity, Intent sourceIntent, @NonNull WebappInfo info) {
Intent launchIntent = new Intent(Intent.ACTION_VIEW, info.uri());
launchIntent.setPackage(info.webApkPackageName());
launchIntent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | ApiCompatibilityUtils.getActivityNewDocumentFlag());
Bundle extras = sourceIntent.getExtras();
if (extras != null) {
launchIntent.putExtras(extras);
}
Intent launchIntent = createRelaunchWebApkIntent(sourceIntent, (WebApkInfo) info);
launchAfterDelay(
launchingActivity.getApplicationContext(), launchIntent, WEBAPK_LAUNCH_DELAY_MS);
ApiCompatibilityUtils.finishAndRemoveTask(launchingActivity);
......@@ -287,30 +318,6 @@ public class WebappLauncherActivity extends Activity {
return launchIntent;
}
/**
* Brings a live WebappActivity back to the foreground if one exists for the given tab ID.
* @param tabId ID of the Tab to bring back to the foreground.
* @return True if a live WebappActivity was found, false otherwise.
*/
public static boolean bringWebappToFront(int tabId) {
if (tabId == Tab.INVALID_TAB_ID) return false;
for (WeakReference<Activity> activityRef : ApplicationStatus.getRunningActivities()) {
Activity activity = activityRef.get();
if (activity == null || !(activity instanceof WebappActivity)) continue;
WebappActivity webappActivity = (WebappActivity) activity;
if (webappActivity.getActivityTab() != null
&& webappActivity.getActivityTab().getId() == tabId) {
Tab tab = webappActivity.getActivityTab();
tab.getTabWebContentsDelegateAndroid().activateContents();
return true;
}
}
return false;
}
/** Tries to create WebappInfo/WebApkInfo for the intent. */
private static WebappInfo tryCreateWebappInfo(Intent intent) {
// Builds WebApkInfo for the intent if the WebAPK package specified in the intent is a valid
......
......@@ -5,9 +5,11 @@
package org.chromium.chrome.browser.firstrun;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserManager;
import android.support.customtabs.CustomTabsIntent;
......@@ -21,13 +23,23 @@ import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowApplication;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.ShortcutHelper;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.searchwidget.SearchActivity;
import org.chromium.chrome.browser.webapps.TransparentSplashWebApkActivity;
import org.chromium.chrome.browser.webapps.WebApkActivity;
import org.chromium.chrome.browser.webapps.WebApkActivity0;
import org.chromium.chrome.browser.webapps.WebappLauncherActivity;
import org.chromium.webapk.lib.client.WebApkValidator;
import org.chromium.webapk.lib.common.WebApkConstants;
import org.chromium.webapk.lib.common.WebApkMetaDataKeys;
import org.chromium.webapk.test.WebApkTestHelper;
/** JUnit tests for first run triggering code. */
@RunWith(BaseRobolectricTestRunner.class)
......@@ -49,6 +61,40 @@ public final class FirstRunIntegrationUnitTest {
mShadowApplication.setSystemService(Context.USER_SERVICE, userManager);
FirstRunStatus.setFirstRunFlowComplete(false);
WebApkValidator.disableValidationForTesting();
}
/** Checks that the intent component is one of the provided classes. */
private boolean checkIntentComponentClassOneOf(Intent intent, Class[] componentClassOptions) {
if (intent == null || intent.getComponent() == null) return false;
String componentClassName = intent.getComponent().getClassName();
for (Class componentClassOption : componentClassOptions) {
if (componentClassOption.getName().equals(componentClassName)) return true;
}
return false;
}
/**
* Checks that intent is either for {@link FirstRunActivity} or
* {@link TabbedModeFirstRunActivity}.
*/
private boolean checkIntentIsForFre(Intent intent) {
return checkIntentComponentClassOneOf(
intent, new Class[] {FirstRunActivity.class, TabbedModeFirstRunActivity.class});
}
/** Builds activity using the component class name from the provided intent. */
@SuppressWarnings("unchecked")
private static void buildActivityWithClassNameFromIntent(Intent intent) {
Class<? extends Activity> activityClass = null;
try {
activityClass =
(Class<? extends Activity>) Class.forName(intent.getComponent().getClassName());
} catch (ClassNotFoundException e) {
Assert.fail();
}
Robolectric.buildActivity(activityClass, intent).create();
}
/**
......@@ -59,9 +105,7 @@ public final class FirstRunIntegrationUnitTest {
Intent launchedIntent = mShadowApplication.getNextStartedActivity();
Assert.assertNotNull(launchedIntent);
String launchedActivityClassName = launchedIntent.getComponent().getClassName();
Assert.assertTrue(launchedActivityClassName.equals(FirstRunActivity.class.getName())
|| launchedActivityClassName.equals(TabbedModeFirstRunActivity.class.getName()));
Assert.assertTrue(checkIntentIsForFre(launchedIntent));
}
@Test
......@@ -111,4 +155,43 @@ public final class FirstRunIntegrationUnitTest {
assertFirstRunActivityLaunched();
Assert.assertTrue(searchActivity.isFinishing());
}
/**
* Tests that when the first run experience is shown by a WebAPK that the WebAPK is launched
* when the user finishes the first run experience. In the case where the WebAPK (as opposed
* to WebApkActivity) displays the splash screen this is necessary for correct behaviour when
* the user taps the app icon and the WebAPK is still running.
*/
@Test
public void testFreRelaunchesWebApkNotWebApkActivity() {
String webApkPackageName = "org.chromium.webapk.name";
String startUrl = "https://pwa.rocks/";
Bundle bundle = new Bundle();
bundle.putString(WebApkMetaDataKeys.START_URL, startUrl);
WebApkTestHelper.registerWebApkWithMetaData(webApkPackageName, bundle);
WebApkTestHelper.addIntentFilterForUrl(webApkPackageName, startUrl);
Intent intent = new Intent();
intent.putExtra(WebApkConstants.EXTRA_WEBAPK_PACKAGE_NAME, webApkPackageName);
intent.putExtra(ShortcutHelper.EXTRA_URL, startUrl);
intent.putExtra(WebApkConstants.EXTRA_USE_TRANSPARENT_SPLASH, true);
Robolectric.buildActivity(WebappLauncherActivity.class, intent).create();
Intent launchedIntent = mShadowApplication.getNextStartedActivity();
while (checkIntentComponentClassOneOf(launchedIntent,
new Class[] {WebApkActivity.class, WebApkActivity0.class,
TransparentSplashWebApkActivity.class})) {
buildActivityWithClassNameFromIntent(launchedIntent);
launchedIntent = mShadowApplication.getNextStartedActivity();
}
Assert.assertTrue(checkIntentIsForFre(launchedIntent));
PendingIntent freCompleteLaunchIntent = launchedIntent.getParcelableExtra(
FirstRunActivityBase.EXTRA_FRE_COMPLETE_LAUNCH_INTENT);
Assert.assertNotNull(freCompleteLaunchIntent);
Assert.assertEquals(webApkPackageName,
Shadows.shadowOf(freCompleteLaunchIntent).getSavedIntent().getPackage());
}
}
......@@ -4,8 +4,11 @@
package org.chromium.webapk.test;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.os.Bundle;
......@@ -14,11 +17,12 @@ import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.shadows.ShadowPackageManager;
import java.net.URISyntaxException;
/**
* Helper class for WebAPK JUnit tests.
*/
public class WebApkTestHelper {
/**
* Registers WebAPK. This function also creates an empty resource for the WebAPK.
* @param packageName The package to register
......@@ -32,6 +36,19 @@ public class WebApkTestHelper {
packageManager.addPackage(newPackageInfo(packageName, metaData));
}
/** Registers intent filter for the passed-in package name and URL. */
public static void addIntentFilterForUrl(String packageName, String url) {
ShadowPackageManager packageManager =
Shadows.shadowOf(RuntimeEnvironment.application.getPackageManager());
try {
Intent deepLinkIntent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
deepLinkIntent.addCategory(Intent.CATEGORY_BROWSABLE);
deepLinkIntent.setPackage(packageName);
packageManager.addResolveInfoForIntent(deepLinkIntent, newResolveInfo(packageName));
} catch (URISyntaxException e) {
}
}
/** Sets the resource for the given package name. */
public static void setResource(String packageName, Resources res) {
ShadowPackageManager packageManager =
......@@ -47,4 +64,12 @@ public class WebApkTestHelper {
packageInfo.applicationInfo = applicationInfo;
return packageInfo;
}
private static ResolveInfo newResolveInfo(String packageName) {
ActivityInfo activityInfo = new ActivityInfo();
activityInfo.packageName = packageName;
ResolveInfo resolveInfo = new ResolveInfo();
resolveInfo.activityInfo = activityInfo;
return resolveInfo;
}
}
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