| // Copyright 2018 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.webapk.shell_apk.h2o; |
| |
| import android.app.Activity; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.content.pm.ActivityInfo; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.net.Uri; |
| import android.os.Bundle; |
| |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Ignore; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.robolectric.Robolectric; |
| import org.robolectric.RuntimeEnvironment; |
| import org.robolectric.Shadows; |
| import org.robolectric.annotation.Config; |
| import org.robolectric.shadows.ShadowApplication; |
| import org.robolectric.shadows.ShadowPackageManager; |
| |
| import org.chromium.testing.local.LocalRobolectricTestRunner; |
| import org.chromium.webapk.lib.common.WebApkConstants; |
| import org.chromium.webapk.shell_apk.HostBrowserLauncher; |
| import org.chromium.webapk.shell_apk.WebApkSharedPreferences; |
| import org.chromium.webapk.shell_apk.WebApkUtils; |
| |
| import java.util.ArrayList; |
| |
| /** Tests launching WebAPK. */ |
| @RunWith(LocalRobolectricTestRunner.class) |
| @Config(manifest = Config.NONE, packageName = LaunchTest.WEBAPK_PACKAGE_NAME) |
| public final class LaunchTest { |
| /** Values based on manifest specified in GN file. */ |
| public static final String WEBAPK_PACKAGE_NAME = "org.chromium.webapk.h2o.junit_webapk"; |
| private static final String BROWSER_PACKAGE_NAME = "com.google.android.apps.chrome"; |
| private static final String SHARE_ACTIVITY1_CLASS_NAME = |
| "org.chromium.webapk.shell_apk.ShareActivity1"; |
| private static final String DEFAULT_START_URL = "https://pwa.rocks/"; |
| |
| /** Chromium version which does not support showing the splash screen within WebAPK. */ |
| private static final int BROWSER_H2O_INCOMPATIBLE_VERSION = 57; |
| |
| private Context mAppContext; |
| private ShadowApplication mShadowApplication; |
| private PackageManager mPackageManager; |
| private ShadowPackageManager mShadowPackageManager; |
| |
| @Before |
| public void setUp() { |
| mShadowApplication = ShadowApplication.getInstance(); |
| mAppContext = RuntimeEnvironment.application; |
| mPackageManager = mAppContext.getPackageManager(); |
| mShadowPackageManager = Shadows.shadowOf(mPackageManager); |
| } |
| |
| /** |
| * Test launching via a deep link. |
| * Check: |
| * 1) That the host browser was launched. |
| * 2) Which activities were launnched between the activity which handled |
| * the intent and the host browser getting launched. |
| */ |
| @Test |
| @Ignore |
| public void testDeepLink() { |
| final String deepLinkUrl = "https://pwa.rocks/deep.html"; |
| |
| Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(deepLinkUrl)); |
| launchIntent.setPackage(WEBAPK_PACKAGE_NAME); |
| |
| ArrayList<Intent> launchedIntents; |
| launchedIntents = launchAndCheckBrowserLaunched(false /* splashActivityInitiallyEnabled */, |
| false /* browserCompatibleWithSplashActivity */, launchIntent, |
| H2OTransparentLauncherActivity.class, deepLinkUrl); |
| Assert.assertEquals(1, launchedIntents.size()); |
| |
| launchedIntents = launchAndCheckBrowserLaunched(false /* splashActivityInitiallyEnabled */, |
| true /* browserCompatibleWithSplashActivity */, launchIntent, |
| H2OTransparentLauncherActivity.class, deepLinkUrl); |
| Assert.assertEquals(5, launchedIntents.size()); |
| assertIntentComponentClassNameEquals(H2OMainActivity.class, launchedIntents.get(0)); |
| Assert.assertEquals(BROWSER_PACKAGE_NAME, launchedIntents.get(1).getPackage()); |
| assertIntentComponentClassNameEquals( |
| H2OTransparentLauncherActivity.class, launchedIntents.get(2)); |
| assertIntentComponentClassNameEquals(SplashActivity.class, launchedIntents.get(3)); |
| |
| launchedIntents = launchAndCheckBrowserLaunched(true /* splashActivityInitiallyEnabled */, |
| false /* browserCompatibleWithSplashActivity */, launchIntent, |
| H2OTransparentLauncherActivity.class, deepLinkUrl); |
| Assert.assertEquals(2, launchedIntents.size()); |
| assertIntentComponentClassNameEquals(SplashActivity.class, launchedIntents.get(0)); |
| |
| launchedIntents = launchAndCheckBrowserLaunched(true /* splashActivityInitiallyEnabled */, |
| true /* browserCompatibleWithSplashActivity */, launchIntent, |
| H2OTransparentLauncherActivity.class, deepLinkUrl); |
| Assert.assertEquals(2, launchedIntents.size()); |
| assertIntentComponentClassNameEquals(SplashActivity.class, launchedIntents.get(0)); |
| } |
| |
| /** Test that the host browser is launched as a result of a main launch intent. */ |
| @Test |
| @Ignore |
| public void testMainIntent() { |
| Intent launchIntent = new Intent(Intent.ACTION_MAIN); |
| launchIntent.setPackage(WEBAPK_PACKAGE_NAME); |
| |
| ArrayList<Intent> launchedIntents; |
| launchedIntents = launchAndCheckBrowserLaunched(false /* splashActivityInitiallyEnabled */, |
| false /* browserCompatibleWithSplashActivity */, launchIntent, |
| H2OMainActivity.class, DEFAULT_START_URL); |
| Assert.assertEquals(1, launchedIntents.size()); |
| |
| launchedIntents = launchAndCheckBrowserLaunched(false /* splashActivityInitiallyEnabled */, |
| true /* browserCompatibleWithSplashActivity */, launchIntent, H2OMainActivity.class, |
| DEFAULT_START_URL); |
| Assert.assertEquals(4, launchedIntents.size()); |
| Assert.assertEquals(BROWSER_PACKAGE_NAME, launchedIntents.get(0).getPackage()); |
| assertIntentComponentClassNameEquals( |
| H2OTransparentLauncherActivity.class, launchedIntents.get(1)); |
| assertIntentComponentClassNameEquals(SplashActivity.class, launchedIntents.get(2)); |
| |
| launchedIntents = launchAndCheckBrowserLaunched(true /* splashActivityInitiallyEnabled */, |
| false /* browserCompatibleWithSplashActivity */, launchIntent, SplashActivity.class, |
| DEFAULT_START_URL); |
| Assert.assertEquals(1, launchedIntents.size()); |
| |
| launchedIntents = launchAndCheckBrowserLaunched(true /* splashActivityInitiallyEnabled */, |
| true /* browserCompatibleWithSplashActivity */, launchIntent, SplashActivity.class, |
| DEFAULT_START_URL); |
| Assert.assertEquals(1, launchedIntents.size()); |
| } |
| |
| /** |
| * Tests that the target share activity is propagated to the host browser launch intent in |
| * the scenario where there are several hops between the share intent getting handled and the |
| * browser getting launched. |
| */ |
| @Test |
| @Ignore |
| public void testTargetShareActivityPreserved() { |
| final String expectedStartUrl = "https://pwa.rocks/share_title.html?text=subject_value"; |
| |
| Intent launchIntent = new Intent(Intent.ACTION_SEND); |
| launchIntent.setComponent( |
| new ComponentName(WEBAPK_PACKAGE_NAME, SHARE_ACTIVITY1_CLASS_NAME)); |
| launchIntent.putExtra(Intent.EXTRA_TEXT, "subject_value"); |
| |
| ArrayList<Intent> launchedIntents = |
| launchAndCheckBrowserLaunched(true /* splashActivityInitiallyEnabled */, |
| false /* browserCompatibleWithSplashActivity */, launchIntent, |
| H2OTransparentLauncherActivity.class, expectedStartUrl); |
| Assert.assertTrue(launchedIntents.size() > 1); |
| |
| Intent browserLaunchIntent = launchedIntents.get(launchedIntents.size() - 1); |
| Assert.assertEquals(SHARE_ACTIVITY1_CLASS_NAME, |
| browserLaunchIntent.getStringExtra( |
| WebApkConstants.EXTRA_WEBAPK_SELECTED_SHARE_TARGET_ACTIVITY_CLASS_NAME)); |
| } |
| |
| /** |
| * Tests that the EXTRA_SOURCE intent extra in the launch intent is propagated to the host |
| * browser launch intent in the scenario where there are several activity hops between |
| * the deep link getting handled and the host browser getting launched. |
| */ |
| @Test |
| @Ignore |
| public void testSourcePropagated() { |
| final String deepLinkUrl = "https://pwa.rocks/deep_link.html"; |
| final int source = 2; |
| |
| Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(deepLinkUrl)); |
| launchIntent.setPackage(WEBAPK_PACKAGE_NAME); |
| launchIntent.putExtra(WebApkConstants.EXTRA_SOURCE, source); |
| |
| ArrayList<Intent> launchedIntents = |
| launchAndCheckBrowserLaunched(true /* splashActivityInitiallyEnabled */, |
| true /* browserCompatibleWithSplashActivity */, launchIntent, |
| H2OTransparentLauncherActivity.class, deepLinkUrl); |
| Assert.assertTrue(launchedIntents.size() > 1); |
| |
| Intent browserLaunchIntent = launchedIntents.get(launchedIntents.size() - 1); |
| Assert.assertEquals( |
| source, browserLaunchIntent.getIntExtra(WebApkConstants.EXTRA_SOURCE, -1)); |
| } |
| |
| /** |
| * Check that the WebAPK does not propagate the {@link EXTRA_RELAUNCH} extra. When |
| * the host browser relaunches the WebAPK, the host browser might copy over all of |
| * the extras and not remove the relaunch intent. Check that this scenario does not |
| * yield an infinite loop. |
| */ |
| @Test |
| @Ignore |
| public void testDoesNotPropagateRelaunchDirective() throws Exception { |
| final String deepLinkUrl = "https://pwa.rocks/deep_link.html"; |
| |
| Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(deepLinkUrl)); |
| launchIntent.setPackage(WEBAPK_PACKAGE_NAME); |
| launchIntent.putExtra(WebApkConstants.EXTRA_RELAUNCH, true); |
| |
| ArrayList<Intent> launchedIntents = |
| launchAndCheckBrowserLaunched(true /* splashActivityInitiallyEnabled */, |
| true /* browserCompatibleWithSplashActivity */, launchIntent, |
| H2OTransparentLauncherActivity.class, deepLinkUrl); |
| Assert.assertTrue(launchedIntents.size() > 1); |
| |
| Intent browserLaunchIntent = launchedIntents.get(launchedIntents.size() - 1); |
| Assert.assertFalse(browserLaunchIntent.hasExtra(WebApkConstants.EXTRA_RELAUNCH)); |
| } |
| |
| /** |
| * Test that WebAPK does not keep asking the host browser to relaunch the WebAPK if changing the |
| * enabled component is slow. |
| */ |
| @Test |
| @Ignore |
| public void testDoesNotLoopIfEnablingSplashActivityIsSlow() { |
| // SplashActivity is disabled. Host browser is compatible with SplashActivity. |
| changeWebApkActivityEnabledSetting(mPackageManager, SplashActivity.class, |
| PackageManager.COMPONENT_ENABLED_STATE_DISABLED); |
| changeWebApkActivityEnabledSetting(mPackageManager, H2OMainActivity.class, |
| PackageManager.COMPONENT_ENABLED_STATE_ENABLED); |
| installBrowser( |
| BROWSER_PACKAGE_NAME, H2OLauncher.MINIMUM_REQUIRED_CHROMIUM_VERSION_NEW_SPLASH); |
| |
| Intent launchIntent = new Intent(Intent.ACTION_MAIN); |
| launchIntent.setPackage(WEBAPK_PACKAGE_NAME); |
| |
| // WebAPK requested host browser to relaunch WebAPK recently. The WebAPK should not ask |
| // the host browser to relaunch it again. |
| { |
| SharedPreferences.Editor editor = WebApkSharedPreferences.getPrefs(mAppContext).edit(); |
| editor.putLong( |
| WebApkSharedPreferences.SHARED_PREF_REQUEST_HOST_BROWSER_RELAUNCH_TIMESTAMP, |
| System.currentTimeMillis() - 1); |
| editor.apply(); |
| |
| Robolectric.buildActivity(H2OMainActivity.class, launchIntent).create(); |
| Intent startedActivityIntent = mShadowApplication.getNextStartedActivity(); |
| Assert.assertEquals(BROWSER_PACKAGE_NAME, startedActivityIntent.getPackage()); |
| Assert.assertFalse(startedActivityIntent.hasExtra(WebApkConstants.EXTRA_RELAUNCH)); |
| } |
| |
| // WebAPK requested host browser to relaunch WebAPK a long time ago. The WebAPK should ask |
| // the host browser to relaunch it. |
| { |
| SharedPreferences.Editor editor = WebApkSharedPreferences.getPrefs(mAppContext).edit(); |
| editor.putLong( |
| WebApkSharedPreferences.SHARED_PREF_REQUEST_HOST_BROWSER_RELAUNCH_TIMESTAMP, 1); |
| editor.apply(); |
| |
| Robolectric.buildActivity(H2OMainActivity.class, launchIntent).create(); |
| Intent startedActivityIntent = mShadowApplication.getNextStartedActivity(); |
| Assert.assertEquals(BROWSER_PACKAGE_NAME, startedActivityIntent.getPackage()); |
| Assert.assertTrue(startedActivityIntent.hasExtra(WebApkConstants.EXTRA_RELAUNCH)); |
| } |
| } |
| |
| /** Checks the name of the intent's component class name. */ |
| private static void assertIntentComponentClassNameEquals(Class expectedClass, Intent intent) { |
| Assert.assertEquals(expectedClass.getName(), intent.getComponent().getClassName()); |
| } |
| |
| /** |
| * Launches WebAPK with the given intent and configuration. Tests that the host browser is |
| * launched and which activities are enabled after the browser launch. |
| * @param splashActivityInitiallyEnabled Whether SplashActivity is enabled at the beginning |
| * of the test case. |
| * @param browserCompatibleWithSplashActivity Whether the host browser supports the ShellAPK |
| * showing the splash screen. |
| * @param launchIntent Intent to launch. |
| * @param launchActivity Activity which should receive the launch intent. |
| * @param expectedLaunchUrl The expected launch URL. |
| * @return List of launched activity intents (including the host browser launch intent). |
| */ |
| private ArrayList<Intent> launchAndCheckBrowserLaunched(boolean splashActivityInitiallyEnabled, |
| boolean browserCompatibleWithSplashActivity, Intent launchIntent, |
| Class<? extends Activity> launchActivity, String expectedLaunchUrl) { |
| changeWebApkActivityEnabledSetting(mPackageManager, |
| splashActivityInitiallyEnabled ? SplashActivity.class : H2OMainActivity.class, |
| PackageManager.COMPONENT_ENABLED_STATE_ENABLED); |
| changeWebApkActivityEnabledSetting(mPackageManager, |
| splashActivityInitiallyEnabled ? H2OMainActivity.class : SplashActivity.class, |
| PackageManager.COMPONENT_ENABLED_STATE_DISABLED); |
| installBrowser(BROWSER_PACKAGE_NAME, |
| browserCompatibleWithSplashActivity |
| ? H2OLauncher.MINIMUM_REQUIRED_CHROMIUM_VERSION_NEW_SPLASH |
| : BROWSER_H2O_INCOMPATIBLE_VERSION); |
| |
| // Android modifies the intent when the intent is used to launch an activity. Clone the |
| // intent so as not to affect test cases which use the same intent. |
| Intent launchIntentCopy = (Intent) launchIntent.clone(); |
| |
| ArrayList<Intent> launchedIntents = |
| runActivityChain(launchIntentCopy, launchActivity, BROWSER_PACKAGE_NAME); |
| |
| Assert.assertTrue(!launchedIntents.isEmpty()); |
| Intent browserLaunchIntent = launchedIntents.get(launchedIntents.size() - 1); |
| Assert.assertEquals( |
| HostBrowserLauncher.ACTION_START_WEBAPK, browserLaunchIntent.getAction()); |
| Assert.assertEquals( |
| expectedLaunchUrl, browserLaunchIntent.getStringExtra(WebApkConstants.EXTRA_URL)); |
| |
| Assert.assertEquals(browserCompatibleWithSplashActivity, |
| isWebApkActivityEnabled(mPackageManager, SplashActivity.class)); |
| Assert.assertEquals(!browserCompatibleWithSplashActivity, |
| isWebApkActivityEnabled(mPackageManager, H2OMainActivity.class)); |
| |
| return launchedIntents; |
| } |
| |
| /** Changes whether the passed in WebAPK activity is enabled. */ |
| private static void changeWebApkActivityEnabledSetting( |
| PackageManager packageManager, Class<? extends Activity> activity, int enabledSetting) { |
| ComponentName component = new ComponentName(WEBAPK_PACKAGE_NAME, activity.getName()); |
| packageManager.setComponentEnabledSetting( |
| component, enabledSetting, PackageManager.DONT_KILL_APP); |
| } |
| |
| /** Returns whether the passed in WebAPK activity is enabled. */ |
| private static boolean isWebApkActivityEnabled( |
| PackageManager packageManager, Class<? extends Activity> activity) { |
| ComponentName component = new ComponentName(WEBAPK_PACKAGE_NAME, activity.getName()); |
| int enabledSetting = packageManager.getComponentEnabledSetting(component); |
| return (enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED); |
| } |
| |
| /** |
| * Launches activity with the given intent. Runs till the browser package is launched. Returns |
| * the chain of launched activities (including the browser launch). |
| */ |
| @SuppressWarnings("unchecked") |
| private ArrayList<Intent> runActivityChain( |
| Intent launchIntent, Class<? extends Activity> launchActivity, String browserPackage) { |
| ArrayList<Intent> activityIntentChain = new ArrayList<Intent>(); |
| |
| Robolectric.buildActivity(launchActivity, launchIntent).create(); |
| for (;;) { |
| Intent startedActivityIntent = mShadowApplication.getNextStartedActivity(); |
| if (startedActivityIntent == null) break; |
| |
| activityIntentChain.add(startedActivityIntent); |
| |
| if (browserPackage.equals(startedActivityIntent.getPackage())) { |
| if (!startedActivityIntent.hasExtra(WebApkConstants.EXTRA_RELAUNCH)) break; |
| |
| // Emulate host browser relaunch behaviour. |
| String startUrl = startedActivityIntent.getStringExtra(WebApkConstants.EXTRA_URL); |
| Intent relaunchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(startUrl)); |
| relaunchIntent.setComponent(new ComponentName( |
| WEBAPK_PACKAGE_NAME, H2OTransparentLauncherActivity.class.getName())); |
| Bundle startedActivityExtras = startedActivityIntent.getExtras(); |
| if (startedActivityExtras != null) { |
| relaunchIntent.putExtras(startedActivityExtras); |
| } |
| mAppContext.startActivity(relaunchIntent); |
| continue; |
| } |
| |
| Class<? extends Activity> startedActivityClass = null; |
| try { |
| startedActivityClass = (Class<? extends Activity>) Class.forName( |
| startedActivityIntent.getComponent().getClassName()); |
| } catch (ClassNotFoundException e) { |
| Assert.fail(); |
| } |
| Robolectric.buildActivity(startedActivityClass, startedActivityIntent).create(); |
| } |
| return activityIntentChain; |
| } |
| |
| /** Installs browser with the given package name and version. */ |
| private void installBrowser(String browserPackageName, int version) { |
| Intent intent = WebApkUtils.getQueryInstalledBrowsersIntent(); |
| mShadowPackageManager.addResolveInfoForIntent(intent, newResolveInfo(browserPackageName)); |
| mShadowPackageManager.addPackage(newPackageInfo(browserPackageName, version)); |
| } |
| |
| private static ResolveInfo newResolveInfo(String packageName) { |
| ActivityInfo activityInfo = new ActivityInfo(); |
| activityInfo.packageName = packageName; |
| ResolveInfo resolveInfo = new ResolveInfo(); |
| resolveInfo.activityInfo = activityInfo; |
| return resolveInfo; |
| } |
| |
| private static PackageInfo newPackageInfo(String packageName, int version) { |
| PackageInfo packageInfo = new PackageInfo(); |
| packageInfo.packageName = packageName; |
| packageInfo.versionName = version + "."; |
| packageInfo.applicationInfo = new ApplicationInfo(); |
| packageInfo.applicationInfo.enabled = true; |
| return packageInfo; |
| } |
| } |