diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c29b36552a47..4bdd7a7263e0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -66,7 +66,7 @@ + + + + + + diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index 443263e49f55..2d2213e36444 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -52,4 +52,9 @@ object FeatureFlags { * Gives option in Settings to Delete Browsing Data on new menu option Quit */ val deleteDataOnQuit = nightly or debug + + /** + * Allows Progressive Web Apps to be installed to the device home screen. + */ + val progressiveWebApps = nightly or debug } diff --git a/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt b/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt index 25642d3520bb..a3821b5396e5 100644 --- a/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt @@ -10,21 +10,24 @@ import android.os.Bundle import android.speech.RecognizerIntent import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch +import mozilla.components.feature.intent.processing.TabIntentProcessor import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.customtabs.AuthCustomTabActivity import org.mozilla.fenix.customtabs.AuthCustomTabActivity.Companion.EXTRA_AUTH_CUSTOM_TAB -import org.mozilla.fenix.customtabs.CustomTabActivity +import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.home.intent.StartSearchIntentProcessor +/** + * Processes incoming intents and sends them to the corresponding activity. + */ class IntentReceiverActivity : Activity() { // Holds the intent that initially started this activity // so that it can persist through the speech activity. private var previousIntent: Intent? = null - @Suppress("ComplexMethod") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -39,17 +42,17 @@ class IntentReceiverActivity : Activity() { // the HomeActivity. val intent = intent?.let { Intent(intent) } ?: Intent() - val intentProcessors = listOf( - components.intentProcessors.customTabIntentProcessor, - components.intentProcessors.intentProcessor - ) + val tabIntentProcessor = components.intentProcessors.intentProcessor + + val intentProcessors = components.intentProcessors.externalAppIntentProcessors + + tabIntentProcessor if (intent.getBooleanExtra(SPEECH_PROCESSING, false)) { previousIntent = intent displaySpeechRecognizer() } else { intentProcessors.any { it.process(intent) } - setIntentActivity(intent) + setIntentActivity(intent, tabIntentProcessor) startActivity(intent) @@ -58,19 +61,22 @@ class IntentReceiverActivity : Activity() { } } - private fun setIntentActivity(intent: Intent) { + /** + * Sets the activity that this [intent] will launch. + */ + private fun setIntentActivity(intent: Intent, tabIntentProcessor: TabIntentProcessor) { val openToBrowser = when { - components.intentProcessors.customTabIntentProcessor.matches(intent) -> { + components.intentProcessors.externalAppIntentProcessors.any { it.matches(intent) } -> { // TODO this needs to change: https://github.com/mozilla-mobile/fenix/issues/5225 val activityClass = if (intent.hasExtra(EXTRA_AUTH_CUSTOM_TAB)) { AuthCustomTabActivity::class } else { - CustomTabActivity::class + ExternalAppBrowserActivity::class } intent.setClassName(applicationContext, activityClass.java.name) true } - intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND -> { + tabIntentProcessor.matches(intent) -> { intent.setClassName(applicationContext, HomeActivity::class.java.name) // This Intent was launched from history (recent apps). Android will redeliver the // original Intent (which might be a VIEW intent). However if there's no active browsing diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index bef44d40bd45..79c736f31863 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -344,15 +344,7 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs toolbar.visibility = View.VISIBLE nestedScrollQuickAction.visibility = View.VISIBLE } - view.swipeRefresh.apply { - val (topMargin, bottomMargin) = if (inFullScreen) 0 to 0 else getEngineMargins() - (layoutParams as CoordinatorLayout.LayoutParams).setMargins( - 0, - topMargin, - 0, - bottomMargin - ) - } + updateLayoutMargins(inFullScreen) }, owner = this, view = view @@ -526,6 +518,13 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs */ protected abstract fun getAppropriateLayoutGravity(): Int + protected fun updateLayoutMargins(inFullScreen: Boolean) { + view?.swipeRefresh?.apply { + val (topMargin, bottomMargin) = if (inFullScreen) 0 to 0 else getEngineMargins() + (layoutParams as CoordinatorLayout.LayoutParams).setMargins(0, topMargin, 0, bottomMargin) + } + } + /** * Updates the site permissions rules based on user settings. */ diff --git a/app/src/main/java/org/mozilla/fenix/components/IntentProcessors.kt b/app/src/main/java/org/mozilla/fenix/components/IntentProcessors.kt index 99f7720babc4..59e7c14c9bab 100644 --- a/app/src/main/java/org/mozilla/fenix/components/IntentProcessors.kt +++ b/app/src/main/java/org/mozilla/fenix/components/IntentProcessors.kt @@ -8,6 +8,8 @@ import android.content.Context import mozilla.components.browser.session.SessionManager import mozilla.components.feature.customtabs.CustomTabIntentProcessor import mozilla.components.feature.intent.processing.TabIntentProcessor +import mozilla.components.feature.pwa.ManifestStorage +import mozilla.components.feature.pwa.intent.WebAppIntentProcessor import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.session.SessionUseCases import org.mozilla.fenix.test.Mockable @@ -36,7 +38,10 @@ class IntentProcessors( TabIntentProcessor(sessionManager, sessionUseCases.loadUrl, searchUseCases.newTabSearch, isPrivate = true) } - val customTabIntentProcessor by lazy { - CustomTabIntentProcessor(sessionManager, sessionUseCases.loadUrl, context.resources) + val externalAppIntentProcessors by lazy { + listOf( + WebAppIntentProcessor(sessionManager, sessionUseCases.loadUrl, ManifestStorage(context)), + CustomTabIntentProcessor(sessionManager, sessionUseCases.loadUrl, context.resources) + ) } } diff --git a/app/src/main/java/org/mozilla/fenix/components/UseCases.kt b/app/src/main/java/org/mozilla/fenix/components/UseCases.kt index 850c9e907de9..2b6a6e591392 100644 --- a/app/src/main/java/org/mozilla/fenix/components/UseCases.kt +++ b/app/src/main/java/org/mozilla/fenix/components/UseCases.kt @@ -17,6 +17,7 @@ import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SettingsUseCases import mozilla.components.feature.tabs.TabsUseCases +import org.mozilla.fenix.FeatureFlags.progressiveWebApps import org.mozilla.fenix.test.Mockable /** @@ -53,7 +54,9 @@ class UseCases( val appLinksUseCases by lazy { AppLinksUseCases(context.applicationContext) } - val webAppUseCases by lazy { WebAppUseCases(context, sessionManager, httpClient, supportWebApps = false) } + val webAppUseCases by lazy { + WebAppUseCases(context, sessionManager, httpClient, supportWebApps = progressiveWebApps) + } val downloadUseCases by lazy { DownloadsUseCases(sessionManager) } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt index 6c7a8d618e55..5003aaff843a 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt @@ -11,9 +11,9 @@ import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem import mozilla.components.browser.menu.item.BrowserMenuImageText import mozilla.components.browser.menu.item.BrowserMenuItemToolbar import mozilla.components.browser.menu.item.BrowserMenuSwitch +import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.ext.asActivity import org.mozilla.fenix.ext.components import org.mozilla.fenix.theme.ThemeManager diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/AuthCustomTabActivity.kt b/app/src/main/java/org/mozilla/fenix/customtabs/AuthCustomTabActivity.kt index 8e74944d4a68..cf1e30f560ac 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/AuthCustomTabActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/AuthCustomTabActivity.kt @@ -12,7 +12,7 @@ import org.mozilla.fenix.ext.components /** * A special custom tab for signing into a Firefox Account. The activity is closed once the user is signed in. */ -class AuthCustomTabActivity : CustomTabActivity() { +class AuthCustomTabActivity : ExternalAppBrowserActivity() { private val accountStateObserver = object : AccountObserver { /** diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabActivity.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt similarity index 65% rename from app/src/main/java/org/mozilla/fenix/customtabs/CustomTabActivity.kt rename to app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt index cf3e162d0ad6..1d2fc1f0a5c6 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt @@ -5,8 +5,12 @@ package org.mozilla.fenix.customtabs import androidx.navigation.NavDestination +import androidx.navigation.NavDirections import mozilla.components.browser.session.runWithSession +import mozilla.components.concept.engine.manifest.WebAppManifestParser import mozilla.components.feature.intent.ext.getSessionId +import mozilla.components.feature.pwa.ext.getTrustedScope +import mozilla.components.feature.pwa.ext.getWebAppManifest import mozilla.components.support.utils.SafeIntent import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity @@ -17,7 +21,12 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.theme.CustomTabThemeManager import java.security.InvalidParameterException -open class CustomTabActivity : HomeActivity() { +/** + * Activity that holds the [ExternalAppBrowserFragment] that is launched within an external app, + * such as custom tabs and progressive web apps. + */ +open class ExternalAppBrowserActivity : HomeActivity() { + final override fun getBreadcrumbMessage(destination: NavDestination): String { val fragmentName = resources.getResourceEntryName(destination.id) return "Changing to fragment $fragmentName, isCustomTab: true" @@ -30,12 +39,23 @@ open class CustomTabActivity : HomeActivity() { override fun getNavDirections( from: BrowserDirection, customTabSessionId: String? - ) = when (from) { - BrowserDirection.FromGlobal -> - NavGraphDirections.actionGlobalExternalAppBrowser(customTabSessionId) - else -> throw InvalidParameterException( - "Tried to navigate to ExternalAppBrowserFragment from $from" - ) + ): NavDirections { + val manifest = intent.getWebAppManifest() + val manifestJson = manifest?.let { WebAppManifestParser().serialize(it).toString() } + val trustedScopes = listOfNotNull( + manifest?.getTrustedScope()?.toString() + ).toTypedArray() + return when (from) { + BrowserDirection.FromGlobal -> + NavGraphDirections.actionGlobalExternalAppBrowser( + activeSessionId = customTabSessionId, + webAppManifest = manifestJson, + trustedScopes = trustedScopes + ) + else -> throw InvalidParameterException( + "Tried to navigate to ExternalAppBrowserFragment from $from" + ) + } } final override fun createBrowsingModeManager() = diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt index 03d2805621ab..a7deb8f622d0 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt @@ -6,11 +6,19 @@ package org.mozilla.fenix.customtabs import android.view.Gravity import android.view.View +import androidx.core.net.toUri +import androidx.core.view.isGone +import androidx.navigation.fragment.navArgs import kotlinx.android.synthetic.main.component_search.* import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ObsoleteCoroutinesApi import mozilla.components.browser.session.Session +import mozilla.components.concept.engine.manifest.WebAppManifestParser +import mozilla.components.concept.engine.manifest.getOrNull +import mozilla.components.feature.pwa.feature.WebAppActivityFeature +import mozilla.components.feature.pwa.feature.WebAppHideToolbarFeature +import mozilla.components.feature.pwa.feature.WebAppSiteControlsFeature import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.BackHandler @@ -29,7 +37,10 @@ import org.mozilla.fenix.ext.requireComponents @ExperimentalCoroutinesApi class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler { + private val args by navArgs() + private val customTabsIntegration = ViewBoundFeatureWrapper() + private val hideToolbarFeature = ViewBoundFeatureWrapper() override fun initializeUI(view: View): Session? { return super.initializeUI(view)?.also { @@ -41,18 +52,61 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler { requireComponents.core.sessionManager, toolbar, customTabSessionId, - activity, + activity!!, view.nestedScrollQuickAction, view.swipeRefresh, onItemTapped = { browserInteractor.onBrowserToolbarMenuItemTapped(it) } ), owner = this, view = view) + + val trustedScopes = args.trustedScopes.toList().map { it.toUri() } + hideToolbarFeature.set( + feature = WebAppHideToolbarFeature( + requireComponents.core.sessionManager, + toolbar, + customTabSessionId, + trustedScopes + ), + owner = this, + view = toolbar) + // Hot-fix until there's a hideToolbarFeature.onHideStateChanged + if (trustedScopes.isNotEmpty()) { + getSessionById()?.register(object : Session.Observer { + override fun onUrlChanged(session: Session, url: String) { + updateLayoutMargins(false) + } + }) + } + + val manifest = args.webAppManifest?.let { json -> + WebAppManifestParser().parse(json).getOrNull() + } + if (manifest != null) { + activity?.lifecycle?.addObserver( + WebAppActivityFeature( + activity!!, + requireComponents.core.icons, + manifest + ) + ) + activity?.lifecycle?.addObserver( + WebAppSiteControlsFeature( + context?.applicationContext!!, + requireComponents.core.sessionManager, + requireComponents.useCases.sessionUseCases.reload, + customTabSessionId, + manifest + ) + ) + } } consumeFrom(browserStore) { browserToolbarView.update(it) } + + updateLayoutMargins(false) } } @@ -91,8 +145,13 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler { } override fun getEngineMargins(): Pair { - val toolbarSize = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height) - return toolbarSize to 0 + val toolbarHidden = toolbar.isGone + return if (toolbarHidden) { + 0 to 0 + } else { + val toolbarSize = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height) + toolbarSize to 0 + } } override fun getAppropriateLayoutGravity() = Gravity.TOP diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 36a70dae92a7..4a83451fe076 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -202,10 +202,9 @@ android:id="@+id/externalAppBrowserFragment" android:name="org.mozilla.fenix.customtabs.ExternalAppBrowserFragment" tools:layout="@layout/fragment_browser"> - + + + diff --git a/app/src/test/java/org/mozilla/fenix/customtabs/CustomTabActivityTest.kt b/app/src/test/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivityTest.kt similarity index 93% rename from app/src/test/java/org/mozilla/fenix/customtabs/CustomTabActivityTest.kt rename to app/src/test/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivityTest.kt index 04ebd6c1a4ae..6eeea7e4f49a 100644 --- a/app/src/test/java/org/mozilla/fenix/customtabs/CustomTabActivityTest.kt +++ b/app/src/test/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivityTest.kt @@ -18,11 +18,11 @@ import org.robolectric.annotation.Config @ObsoleteCoroutinesApi @RunWith(RobolectricTestRunner::class) @Config(application = TestApplication::class) -class CustomTabActivityTest { +class ExternalAppBrowserActivityTest { @Test fun getIntentSource() { - val activity = CustomTabActivity() + val activity = ExternalAppBrowserActivity() val launcherIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER)