Skip to content

Commit

Permalink
PWA: Add activities for routing and handling standalone/fullscreen apps.
Browse files Browse the repository at this point in the history
  • Loading branch information
pocmo committed Mar 21, 2019
1 parent e8a3762 commit bc2acaf
Show file tree
Hide file tree
Showing 7 changed files with 414 additions and 1 deletion.
24 changes: 24 additions & 0 deletions components/feature/pwa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,30 @@ Use Gradle to download the library from [maven.mozilla.org](https://maven.mozill
implementation "org.mozilla.components:feature-pwa:{latest-version}"
```

### WebAppShellActivity

Standalone and fullscreen web apps are launched in a `WebAppShellActivity` instance. Since this instance requires access to some components and app using this component needs to create a new class and register it in the manifest:

```Kotlin
class WebAppActivity : AbstractWebAppShellActivity() {
override val engine: Engine by lazy {
/* Get Engine instance */
}
override val sessionManager: SessionManager by lazy {
/* Get SessionManager instance */
}
}
```

```xml
<activity android:name=".WebAppActivity">
<intent-filter>
<action android:name="mozilla.components.feature.pwa.SHELL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
```

## License

This Source Code Form is subject to the terms of the Mozilla Public
Expand Down
1 change: 1 addition & 0 deletions components/feature/pwa/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ android {
dependencies {
implementation project(':browser-session')
implementation project(':concept-engine')
implementation project(':support-base')
implementation project(':support-ktx')

implementation Dependencies.kotlin_stdlib
Expand Down
12 changes: 11 additions & 1 deletion components/feature/pwa/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,14 @@
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="mozilla.components.feature.pwa" />
package="mozilla.components.feature.pwa">

<application>

<activity android:name=".WebAppLauncherActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:exported="true" />

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.pwa

import android.os.Bundle
import android.support.annotation.VisibleForTesting
import android.support.v7.app.AppCompatActivity
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.manifest.WebAppManifest
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.pwa.ext.applyOrientation
import mozilla.components.support.ktx.android.view.enterToImmersiveMode

/**
* Activity for "standalone" and "fullscreen" web applications.
*/
abstract class AbstractWebAppShellActivity : AppCompatActivity() {
abstract val engine: Engine
abstract val sessionManager: SessionManager

lateinit var session: Session
lateinit var engineView: EngineView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// We do not "install" web apps yet. So there's no place we can load a manifest from yet.
// https://github.com/mozilla-mobile/android-components/issues/2382
val manifest = createTestManifest()

applyConfiguration(manifest)
renderSession(manifest)
}

override fun onDestroy() {
super.onDestroy()

sessionManager
.getOrCreateEngineSession(session)
.close()
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun applyConfiguration(manifest: WebAppManifest) {
if (manifest.display == WebAppManifest.DisplayMode.FULLSCREEN) {
enterToImmersiveMode()
}

applyOrientation(manifest)
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun renderSession(manifest: WebAppManifest) {
setContentView(engine
.createView(this)
.also { engineView = it }
.asView())

session = Session(manifest.startUrl)
engineView.render(sessionManager.getOrCreateEngineSession(session))
}

companion object {
const val INTENT_ACTION = "mozilla.components.feature.pwa.SHELL"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.pwa

import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.support.annotation.VisibleForTesting
import android.support.annotation.VisibleForTesting.PRIVATE
import android.support.v7.app.AppCompatActivity
import mozilla.components.browser.session.manifest.WebAppManifest
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.kotlin.toUri

/**
* This activity is launched by Web App shortcuts on the home screen.
*
* Based on the Web App Manifest (display) it will decide whether the app is launched in the browser or in a
* standalone activity.
*/
class WebAppLauncherActivity : AppCompatActivity() {
private val logger = Logger("WebAppLauncherActivity")

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val manifest = loadManifest()

routeManifest(manifest)

finish()
}

@VisibleForTesting(otherwise = PRIVATE)
internal fun routeManifest(manifest: WebAppManifest) {
when (manifest.display) {
WebAppManifest.DisplayMode.FULLSCREEN, WebAppManifest.DisplayMode.STANDALONE -> launchWebAppShell()

// We do not implement "minimal-ui" mode. Following the Web App Manifest spec we fallback to
// using "browser" in this case.
WebAppManifest.DisplayMode.MINIMAL_UI -> launchBrowser(manifest)

WebAppManifest.DisplayMode.BROWSER -> launchBrowser(manifest)
}
}

@VisibleForTesting(otherwise = PRIVATE)
internal fun launchBrowser(manifest: WebAppManifest) {
val intent = Intent(Intent.ACTION_VIEW, manifest.startUrl.toUri())
intent.`package` = packageName

try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
logger.error("Package does not handle VIEW intent. Can't launch browser.")
}
}

@VisibleForTesting(otherwise = PRIVATE)
internal fun launchWebAppShell() {
val intent = Intent()
intent.action = AbstractWebAppShellActivity.INTENT_ACTION
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.`package` = packageName

try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
logger.error("Packages does not handle AbstractWebAppShellActivity intent. Can't launch web app.", e)
}
}

@VisibleForTesting(otherwise = PRIVATE)
internal fun loadManifest(): WebAppManifest {
// We do not "install" web apps yet. So there's no place we can load a manifest from yet.
// https://github.com/mozilla-mobile/android-components/issues/2382
return createTestManifest()
}
}

/**
* Just a test manifest we use for testing until we save and load the actual manifests.
* https://github.com/mozilla-mobile/android-components/issues/2382
*/
internal fun createTestManifest(): WebAppManifest {
return WebAppManifest(
name = "Demo",
startUrl = "https://www.mozilla.org",
display = WebAppManifest.DisplayMode.FULLSCREEN)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.pwa

import android.content.pm.ActivityInfo
import android.view.View
import android.view.Window
import android.view.WindowManager
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.manifest.WebAppManifest
import mozilla.components.concept.engine.Engine
import mozilla.components.support.test.mock
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class AbstractWebAppShellActivityTest {
@Test
fun `applyConfiguration applies orientation`() {
val activity = spy(TestWebAppShellActivity())

val manifest = WebAppManifest(
name = "Test Manifest",
startUrl = "/",
orientation = WebAppManifest.Orientation.LANDSCAPE)

activity.applyConfiguration(manifest)

verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}

@Test
fun `applyConfiguration switches to immersive mode (fullscreen display mode)`() {
val decorView: View = mock()

val window: Window = mock()
doReturn(decorView).`when`(window).decorView

val activity = spy(TestWebAppShellActivity())
doReturn(window).`when`(activity).window

val manifest = WebAppManifest(
name = "Test Manifest",
startUrl = "/",
orientation = WebAppManifest.Orientation.LANDSCAPE,
display = WebAppManifest.DisplayMode.FULLSCREEN)

activity.applyConfiguration(manifest)

verify(window).addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
verify(decorView).systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
}

@Test
fun `applyConfiguration does NOT switch to immersive mode (standalone display mode)`() {
val decorView: View = mock()

val window: Window = mock()
doReturn(decorView).`when`(window).decorView

val activity = spy(TestWebAppShellActivity())
doReturn(window).`when`(activity).window

val manifest = WebAppManifest(
name = "Test Manifest",
startUrl = "/",
orientation = WebAppManifest.Orientation.LANDSCAPE,
display = WebAppManifest.DisplayMode.STANDALONE)

activity.applyConfiguration(manifest)

verify(window, never()).addFlags(ArgumentMatchers.anyInt())
verify(decorView, never()).systemUiVisibility = ArgumentMatchers.anyInt()
}
}

private class TestWebAppShellActivity(
override val engine: Engine = mock(),
override val sessionManager: SessionManager = mock()
) : AbstractWebAppShellActivity()
Loading

0 comments on commit bc2acaf

Please sign in to comment.