Skip to content

Commit

Permalink
Closes mozilla-mobile#2293 - Open custom tabs when leaving scope
Browse files Browse the repository at this point in the history
Refactors PWA code so that it uses a BrowserFragment subclass.
  • Loading branch information
NotWoods committed Jul 23, 2019
1 parent d01cab7 commit 29cea6b
Show file tree
Hide file tree
Showing 25 changed files with 572 additions and 208 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* 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/. */

@file:Suppress("TooManyFunctions")

package mozilla.components.concept.engine.manifest

import android.graphics.Color
Expand Down Expand Up @@ -90,7 +92,11 @@ class WebAppManifestParser {
}
}

private const val HEX_RADIX = 16
fun WebAppManifestParser.Result.get(): WebAppManifest? = when (this) {
is WebAppManifestParser.Result.Success -> manifest
is WebAppManifestParser.Result.Failure -> null
}

private val whitespace = "\\s+".toRegex()

private fun parseDisplayMode(json: JSONObject): WebAppManifest.DisplayMode {
Expand Down
3 changes: 3 additions & 0 deletions components/feature/pwa/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ dependencies {
implementation project(':browser-session')
implementation project(':concept-engine')
implementation project(':concept-fetch')
implementation project(':feature-session')
implementation project(':support-base')
implementation project(':support-ktx')
implementation project(':support-utils')

implementation Dependencies.androidx_browser
implementation Dependencies.androidx_core_ktx
implementation Dependencies.kotlin_stdlib
implementation Dependencies.kotlin_coroutines
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.feature.pwa.intent.WebAppIntentProcessor.Companion.ACTION_VIEW_PWA
import mozilla.components.support.base.log.logger.Logger

/**
Expand Down Expand Up @@ -76,16 +77,14 @@ class WebAppLauncherActivity : AppCompatActivity(), CoroutineScope by MainScope(

@VisibleForTesting(otherwise = PRIVATE)
internal fun launchWebAppShell(startUrl: Uri) {
val intent = Intent(AbstractWebAppShellActivity.INTENT_ACTION).apply {
data = startUrl
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
val intent = Intent(ACTION_VIEW_PWA, startUrl).apply {
`package` = packageName
}

try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
logger.error("Packages does not handle AbstractWebAppShellActivity intent. Can't launch web app.", e)
logger.error("Packages does not handle ACTION_VIEW_PWA intent. Can't launch as web app.", e)
// Fall back to normal browser
launchBrowser(startUrl)
}
Expand All @@ -97,6 +96,6 @@ class WebAppLauncherActivity : AppCompatActivity(), CoroutineScope by MainScope(
}

companion object {
const val INTENT_ACTION = "mozilla.components.feature.pwa.PWA_LAUNCHER"
internal const val ACTION_PWA_LAUNCHER = "mozilla.components.feature.pwa.PWA_LAUNCHER"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import mozilla.components.browser.icons.utils.IconMemoryCache
import mozilla.components.browser.session.Session
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.concept.fetch.Client
import mozilla.components.feature.pwa.WebAppLauncherActivity.Companion.ACTION_PWA_LAUNCHER
import mozilla.components.feature.pwa.ext.installableManifest

private val pwaIconMemoryCache = IconMemoryCache()
Expand Down Expand Up @@ -93,9 +94,8 @@ class WebAppShortcutManager(
* Create a new basic pinned website shortcut using info from the session.
*/
fun buildBasicShortcut(context: Context, session: Session): ShortcutInfoCompat? {
val shortcutIntent = Intent(context, WebAppLauncherActivity::class.java).apply {
action = WebAppLauncherActivity.INTENT_ACTION
data = session.url.toUri()
val shortcutIntent = Intent(Intent.ACTION_VIEW, session.url.toUri()).apply {
`package` = context.packageName
}

val builder = ShortcutInfoCompat.Builder(context, session.url)
Expand All @@ -114,8 +114,10 @@ class WebAppShortcutManager(
*/
suspend fun buildWebAppShortcut(context: Context, manifest: WebAppManifest): ShortcutInfoCompat? {
val shortcutIntent = Intent(context, WebAppLauncherActivity::class.java).apply {
action = WebAppLauncherActivity.INTENT_ACTION
action = ACTION_PWA_LAUNCHER
data = manifest.startUrl.toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
`package` = context.packageName
}

val shortLabel = manifest.shortName ?: manifest.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ import mozilla.components.concept.engine.manifest.WebAppManifest.Orientation
*/
fun Activity.applyOrientation(manifest: WebAppManifest?) {
requestedOrientation = when (manifest?.orientation) {
Orientation.NATURAL, Orientation.ANY -> ActivityInfo.SCREEN_ORIENTATION_USER
Orientation.NATURAL, Orientation.ANY, null -> ActivityInfo.SCREEN_ORIENTATION_USER
Orientation.LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
Orientation.LANDSCAPE_PRIMARY -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
Orientation.LANDSCAPE_SECONDARY -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
Orientation.PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
Orientation.PORTRAIT_PRIMARY -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
Orientation.PORTRAIT_SECONDARY -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
else -> ActivityInfo.SCREEN_ORIENTATION_USER
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* 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.ext

import android.os.Bundle
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.concept.engine.manifest.WebAppManifestParser
import mozilla.components.concept.engine.manifest.get

const val EXTRA_WEB_APP_MANIFEST = "mozilla.components.feature.pwa.EXTRA_WEB_APP_MANIFEST"

fun Bundle.putWebAppManifest(webAppManifest: WebAppManifest) {
val json = WebAppManifestParser().serialize(webAppManifest)
putString(EXTRA_WEB_APP_MANIFEST, json.toString())
}

fun Bundle.getWebAppManifest(): WebAppManifest? {
return getString(EXTRA_WEB_APP_MANIFEST)?.let { json ->
WebAppManifestParser().parse(json).get()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* 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.ext

import android.content.Intent
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.concept.engine.manifest.WebAppManifestParser

fun Intent.putWebAppManifest(webAppManifest: WebAppManifest) {
val json = WebAppManifestParser().serialize(webAppManifest)
putExtra(EXTRA_WEB_APP_MANIFEST, json.toString())
}

fun Intent.getWebAppManifest(): WebAppManifest? {
return extras?.getWebAppManifest()
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package mozilla.components.feature.pwa.ext

import android.app.ActivityManager.TaskDescription
import android.graphics.Bitmap
import mozilla.components.browser.session.tab.CustomTabConfig
import mozilla.components.concept.engine.manifest.WebAppManifest

/**
Expand All @@ -15,5 +16,17 @@ import mozilla.components.concept.engine.manifest.WebAppManifest
* Instead we use the deprecated constructor.
*/
@Suppress("Deprecation")
fun WebAppManifest.asTaskDescription(icon: Bitmap?) =
fun WebAppManifest.toTaskDescription(icon: Bitmap?) =
TaskDescription(name, icon, themeColor ?: 0)

fun WebAppManifest.toCustomTabConfig() =
CustomTabConfig(
id = startUrl,
toolbarColor = themeColor,
closeButtonIcon = null,
disableUrlbarHiding = false,
actionButtonConfig = null,
showShareMenuItem = true,
menuItems = emptyList(),
options = emptyList()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* 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.feature

import android.app.Activity
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.extension.toIconRequest
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.feature.pwa.ext.applyOrientation
import mozilla.components.feature.pwa.ext.toTaskDescription
import mozilla.components.support.ktx.android.view.enterToImmersiveMode
import mozilla.components.support.ktx.android.view.setNavigationBarTheme
import mozilla.components.support.ktx.android.view.setStatusBarTheme

/**
* Feature used to handle window effects for "standalone" and "fullscreen" web apps.
*/
class WebAppActivityFeature(
private val activity: Activity,
private val icons: BrowserIcons,
private val manifest: WebAppManifest?
) : LifecycleObserver {

private val scope = MainScope()

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
if (manifest?.display == WebAppManifest.DisplayMode.FULLSCREEN) {
activity.enterToImmersiveMode()
}

activity.applyOrientation(manifest)

manifest?.themeColor?.let { activity.setStatusBarTheme(it) }
manifest?.backgroundColor?.let { activity.setNavigationBarTheme(it) }

scope.launch {
updateRecentEntry()
}
}

@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
scope.cancel()
}

@VisibleForTesting
internal suspend fun updateRecentEntry() {
val manifest = this.manifest ?: return
val icon = icons.loadIcon(manifest.toIconRequest()).await()

activity.setTaskDescription(manifest.toTaskDescription(icon.bitmap))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* 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.feature

import android.net.Uri
import android.view.View
import androidx.core.net.toUri
import androidx.core.view.isGone
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.support.base.feature.LifecycleAwareFeature

/**
* Initializes and resets the Toolbar for a Custom Tab based on the CustomTabConfig.
*/
class WebAppToolbarFeature(
private val sessionManager: SessionManager,
private val toolbar: View,
private val sessionId: String,
private val manifest: WebAppManifest?
) : Session.Observer, LifecycleAwareFeature {

init {
toolbar.isGone = true
}

override fun onUrlChanged(session: Session, url: String) {
toolbar.isGone = isInScope(url.toUri())
}

override fun start() {
sessionManager.findSessionById(sessionId)?.register(this)
}

override fun stop() {
sessionManager.findSessionById(sessionId)?.unregister(this)
}

/**
* Checks that the [target] URL is in scope of the web app.
*
* https://www.w3.org/TR/appmanifest/#dfn-within-scope
*/
private fun isInScope(target: Uri): Boolean {
val manifest = this.manifest ?: return false
val scope = (manifest.scope ?: manifest.startUrl).toUri()

return sameOrigin(scope, target) && target.path.orEmpty().startsWith(scope.path.orEmpty())
}

private fun sameOrigin(a: Uri, b: Uri) =
a.scheme == b.scheme && a.host == b.host && a.port == b.port
}
Loading

0 comments on commit 29cea6b

Please sign in to comment.