Commit 603024d0 authored by Tiger Oakes's avatar Tiger Oakes Committed by Tiger Oakes
Browse files

Closes #2382 - Save PWA manifest and shortcuts

parent 48eadc74
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -15,6 +15,8 @@ data class Size(
    val width: Int,
    val height: Int
) {
    override fun toString() = if (this == ANY) "any" else "${width}x$height"

    companion object {
        /**
         * Represents the "any" size.
+49 −2
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ import android.graphics.Color
import androidx.annotation.ColorInt
import mozilla.components.support.ktx.android.org.json.asSequence
import mozilla.components.support.ktx.android.org.json.tryGetString
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

@@ -48,7 +49,7 @@ class WebAppManifestParser {
                name = name,
                shortName = shortName,
                startUrl = json.getString("start_url"),
                display = getDisplayMode(json),
                display = parseDisplayMode(json),
                backgroundColor = parseColor(json.tryGetString("background_color")),
                description = json.tryGetString("description"),
                icons = parseIcons(json),
@@ -62,11 +63,38 @@ class WebAppManifestParser {
            Result.Failure(e)
        }
    }

    /**
     * Parses the provided JSON and returns a [WebAppManifest] (wrapped in [Result.Success] if parsing was successful.
     * Otherwise [Result.Failure].
     */
    fun parse(json: String): Result {
        return try {
            parse(JSONObject(json))
        } catch (e: JSONException) {
            Result.Failure(e)
        }
    }

    fun serialize(manifest: WebAppManifest) = JSONObject().apply {
        put("name", manifest.name)
        putOpt("short_name", manifest.shortName)
        put("start_url", manifest.startUrl)
        putOpt("display", serializeEnumName(manifest.display.name))
        putOpt("background_color", serializeColor(manifest.backgroundColor))
        putOpt("description", manifest.description)
        putOpt("icons", serializeIcons(manifest.icons))
        putOpt("scope", manifest.scope)
        putOpt("theme_color", serializeColor(manifest.themeColor))
        putOpt("dir", serializeEnumName(manifest.dir.name))
        putOpt("lang", manifest.lang)
        putOpt("orientation", serializeEnumName(manifest.orientation.name))
    }
}

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

private fun getDisplayMode(json: JSONObject): WebAppManifest.DisplayMode {
private fun parseDisplayMode(json: JSONObject): WebAppManifest.DisplayMode {
    return when (json.optString("display")) {
        "standalone" -> WebAppManifest.DisplayMode.STANDALONE
        "fullscreen" -> WebAppManifest.DisplayMode.FULLSCREEN
@@ -148,3 +176,22 @@ private fun parseOrientation(json: JSONObject) = when (json.optString("orientati
    "landscape-secondary" -> WebAppManifest.Orientation.LANDSCAPE_SECONDARY
    else -> WebAppManifest.Orientation.ANY
}

private fun serializeEnumName(name: String) = name.toLowerCase().replace('_', '-')

@Suppress("MagicNumber")
private fun serializeColor(color: Int?): String? = color?.let {
    String.format("#%06X", 0xFFFFFF and color)
}

private fun serializeIcons(icons: List<WebAppManifest.Icon>): JSONArray {
    val list = icons.map { icon ->
        JSONObject().apply {
            put("src", icon.src)
            put("sizes", icon.sizes.joinToString(" ") { it.toString() })
            putOpt("type", icon.type)
            put("purpose", icon.purpose.joinToString(" ") { serializeEnumName(it.name) })
        }
    }
    return JSONArray(list)
}
+45 −4
Original line number Diff line number Diff line
@@ -270,6 +270,14 @@ class WebAppManifestParserTest {
        assertTrue(result is WebAppManifestParser.Result.Failure)
    }

    @Test
    fun `Parsing invalid JSON string`() {
        val json = loadManifestAsString("invalid_json.json")
        val result = WebAppManifestParser().parse(json)

        assertTrue(result is WebAppManifestParser.Result.Failure)
    }

    @Test
    fun `Parsing invalid JSON missing name fields`() {
        val json = loadManifest("invalid_missing_name.json")
@@ -324,12 +332,45 @@ class WebAppManifestParserTest {
        }
    }

    private fun loadManifest(fileName: String): JSONObject =
        JSONObject(javaClass.getResourceAsStream("/manifests/$fileName")!!
    @Test
    fun `Serializing minimal manifest`() {
        val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://mozilla.org")
        val json = WebAppManifestParser().serialize(manifest)

        assertEquals("Mozilla", json.getString("name"))
        assertEquals("https://mozilla.org", json.getString("start_url"))
    }

    @Test
    fun `Serialize and parse W3 typical manifest`() {
        val result = WebAppManifestParser().parse(loadManifest("spec_typical.json"))
        val manifest = (result as WebAppManifestParser.Result.Success).manifest

        assertEquals(
            result,
            WebAppManifestParser().parse(WebAppManifestParser().serialize(manifest))
        )
    }

    @Test
    fun `Serialize and parse unusual manifest`() {
        val result = WebAppManifestParser().parse(loadManifest("unusual.json"))
        val manifest = (result as WebAppManifestParser.Result.Success).manifest

        assertEquals(
            result,
            WebAppManifestParser().parse(WebAppManifestParser().serialize(manifest))
        )
    }

    private fun loadManifestAsString(fileName: String): String =
        javaClass.getResourceAsStream("/manifests/$fileName")!!
            .bufferedReader().use {
                it.readText()
            }.also {
                assertNotNull(it)
            }
        )

    private fun loadManifest(fileName: String): JSONObject =
        JSONObject(loadManifestAsString(fileName))
}
+3 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ dependencies {
    implementation project(':support-base')
    implementation project(':support-ktx')

    implementation Dependencies.androidx_core_ktx
    implementation Dependencies.kotlin_stdlib
    implementation Dependencies.kotlin_coroutines

@@ -34,6 +35,8 @@ dependencies {

    testImplementation Dependencies.androidx_test_core
    testImplementation Dependencies.androidx_test_junit
    testImplementation Dependencies.kotlin_coroutines_test
    testImplementation Dependencies.kotlin_reflect
    testImplementation Dependencies.testing_mockito
    testImplementation Dependencies.testing_robolectric
}
+5 −2
Original line number Diff line number Diff line
@@ -8,8 +8,11 @@

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

            android:exported="true">
            <intent-filter>
                <action android:name="mozilla.components.feature.pwa.PWA_LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
Loading