Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
The Tor Project
Applications
android-components
Commits
041ff7da
Commit
041ff7da
authored
Oct 04, 2019
by
Tiger Oakes
Browse files
Closes #4636 - Add ShareTarget data class
parent
35c1f766
Changes
9
Hide whitespace changes
Inline
Side-by-side
components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/WebAppManifest.kt
View file @
041ff7da
...
...
@@ -60,7 +60,8 @@ data class WebAppManifest(
val
scope
:
String
?
=
null
,
@ColorInt
val
themeColor
:
Int
?
=
null
,
val
relatedApplications
:
List
<
ExternalApplicationResource
>
=
emptyList
(),
val
preferRelatedApplications
:
Boolean
=
false
val
preferRelatedApplications
:
Boolean
=
false
,
val
shareTarget
:
ShareTarget
?
=
null
)
{
/**
* Defines the developers’ preferred display mode for the website.
...
...
@@ -191,4 +192,62 @@ data class WebAppManifest(
val
value
:
String
)
}
/**
* Used to define how the web app receives share data.
* If present, a share target should be created so that other Android apps can share to this web app.
*
* @property action URL to open on share
* @property method Method to use with [action]. Either "GET" or "POST".
* @property encType MIME type to specify how the params are encoded.
* @property params Specifies what query parameters correspond to share data.
*/
data class
ShareTarget
(
val
action
:
String
,
val
method
:
RequestMethod
=
RequestMethod
.
GET
,
val
encType
:
EncodingType
=
EncodingType
.
URL_ENCODED
,
val
params
:
Params
=
Params
()
)
{
/**
* Specifies what query parameters correspond to share data.
*
* @property title Name of the query parameter used for the title of the data being shared.
* @property text Name of the query parameter used for the body of the data being shared.
* @property url Name of the query parameter used for a URL referring to a shared resource.
* @property files Form fields used to share files.
*/
data class
Params
(
val
title
:
String
?
=
null
,
val
text
:
String
?
=
null
,
val
url
:
String
?
=
null
,
val
files
:
List
<
Files
>
=
emptyList
()
)
/**
* Specifies a form field member used to share files.
*
* @property name Name of the form field.
* @property accept Accepted MIME types or file extensions.
*/
data class
Files
(
val
name
:
String
,
val
accept
:
List
<
String
>
)
/**
* Valid HTTP methods for [ShareTarget.method].
*/
enum
class
RequestMethod
{
GET
,
POST
}
/**
* Valid encoding MIME types for [ShareTarget.encType].
*/
enum
class
EncodingType
(
val
type
:
String
)
{
URL_ENCODED
(
"application/x-www-form-urlencoded"
),
MULTIPART
(
"multipart/form-data"
)
}
}
}
components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/WebAppManifestParser.kt
View file @
041ff7da
...
...
@@ -8,6 +8,10 @@ package mozilla.components.concept.engine.manifest
import
android.graphics.Color
import
androidx.annotation.ColorInt
import
mozilla.components.concept.engine.manifest.parser.ShareTargetParser
import
mozilla.components.concept.engine.manifest.parser.parseIcons
import
mozilla.components.concept.engine.manifest.parser.serializeEnumName
import
mozilla.components.concept.engine.manifest.parser.serializeIcons
import
mozilla.components.support.ktx.android.org.json.asSequence
import
mozilla.components.support.ktx.android.org.json.tryGetString
import
org.json.JSONArray
...
...
@@ -61,7 +65,8 @@ class WebAppManifestParser {
lang
=
json
.
tryGetString
(
"lang"
),
orientation
=
parseOrientation
(
json
),
relatedApplications
=
parseRelatedApplications
(
json
),
preferRelatedApplications
=
json
.
optBoolean
(
"prefer_related_applications"
,
false
)
preferRelatedApplications
=
json
.
optBoolean
(
"prefer_related_applications"
,
false
),
shareTarget
=
ShareTargetParser
.
parse
(
json
.
optJSONObject
(
"share_target"
))
))
}
catch
(
e
:
JSONException
)
{
Result
.
Failure
(
e
)
...
...
@@ -94,6 +99,7 @@ class WebAppManifestParser {
putOpt
(
"orientation"
,
serializeEnumName
(
manifest
.
orientation
.
name
))
put
(
"related_applications"
,
serializeRelatedApplications
(
manifest
.
relatedApplications
))
put
(
"prefer_related_applications"
,
manifest
.
preferRelatedApplications
)
putOpt
(
"share_target"
,
ShareTargetParser
.
serialize
(
manifest
.
shareTarget
))
}
}
...
...
components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/parser/ShareTargetParser.kt
0 → 100644
View file @
041ff7da
/* 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.concept.engine.manifest.parser
import
mozilla.components.concept.engine.manifest.WebAppManifest.ShareTarget
import
mozilla.components.support.ktx.android.org.json.asSequence
import
mozilla.components.support.ktx.android.org.json.toJSONArray
import
mozilla.components.support.ktx.android.org.json.tryGetString
import
org.json.JSONArray
import
org.json.JSONObject
import
java.util.Locale
internal
object
ShareTargetParser
{
/**
* Parses a share target inside a web app manifest.
*/
fun
parse
(
json
:
JSONObject
?):
ShareTarget
?
{
val
action
=
json
?.
tryGetString
(
"action"
)
?:
return
null
val
method
=
parseMethod
(
json
.
tryGetString
(
"method"
))
val
encType
=
parseEncType
(
json
.
tryGetString
(
"enctype"
))
val
params
=
json
.
optJSONObject
(
"params"
)
return
if
(
method
!=
null
&&
encType
!=
null
&&
validMethodAndEncType
(
method
,
encType
))
{
return
ShareTarget
(
action
=
action
,
method
=
method
,
encType
=
encType
,
params
=
ShareTarget
.
Params
(
title
=
params
?.
tryGetString
(
"title"
),
text
=
params
?.
tryGetString
(
"text"
),
url
=
params
?.
tryGetString
(
"url"
),
files
=
parseFiles
(
params
)
)
)
}
else
{
null
}
}
/**
* Serializes a share target to JSON for a web app manifest.
*/
fun
serialize
(
shareTarget
:
ShareTarget
?):
JSONObject
?
{
shareTarget
?:
return
null
return
JSONObject
().
apply
{
put
(
"action"
,
shareTarget
.
action
)
put
(
"method"
,
shareTarget
.
method
.
name
)
put
(
"enctype"
,
shareTarget
.
encType
.
type
)
val
params
=
JSONObject
().
apply
{
put
(
"title"
,
shareTarget
.
params
.
title
)
put
(
"text"
,
shareTarget
.
params
.
text
)
put
(
"url"
,
shareTarget
.
params
.
url
)
put
(
"files"
,
shareTarget
.
params
.
files
.
asSequence
()
.
map
{
file
->
JSONObject
().
apply
{
put
(
"name"
,
file
.
name
)
putOpt
(
"accept"
,
file
.
accept
.
toJSONArray
())
}
}
.
asIterable
()
.
toJSONArray
())
}
put
(
"params"
,
params
)
}
}
/**
* Convert string to [ShareTarget.RequestMethod]. Returns null if the string is invalid.
*/
private
fun
parseMethod
(
method
:
String
?):
ShareTarget
.
RequestMethod
?
{
method
?:
return
ShareTarget
.
RequestMethod
.
GET
return
try
{
ShareTarget
.
RequestMethod
.
valueOf
(
method
.
toUpperCase
(
Locale
.
ROOT
))
}
catch
(
e
:
IllegalArgumentException
)
{
null
}
}
/**
* Convert string to [ShareTarget.EncodingType]. Returns null if the string is invalid.
*/
private
fun
parseEncType
(
encType
:
String
?):
ShareTarget
.
EncodingType
?
{
val
typeString
=
encType
?.
toLowerCase
(
Locale
.
ROOT
)
?:
return
ShareTarget
.
EncodingType
.
URL_ENCODED
return
ShareTarget
.
EncodingType
.
values
().
find
{
it
.
type
==
typeString
}
}
/**
* Checks that [encType] is URL_ENCODED (if [method] is GET or POST) or MULTIPART (only if POST)
*/
private
fun
validMethodAndEncType
(
method
:
ShareTarget
.
RequestMethod
,
encType
:
ShareTarget
.
EncodingType
)
=
when
(
encType
)
{
ShareTarget
.
EncodingType
.
URL_ENCODED
->
true
ShareTarget
.
EncodingType
.
MULTIPART
->
method
==
ShareTarget
.
RequestMethod
.
POST
}
private
fun
parseFiles
(
params
:
JSONObject
?)
=
when
(
val
files
=
params
?.
opt
(
"files"
))
{
is
JSONObject
->
listOfNotNull
(
parseFile
(
files
))
is
JSONArray
->
files
.
asSequence
{
i
->
getJSONObject
(
i
)
}
.
mapNotNull
(
::
parseFile
)
.
toList
()
else
->
emptyList
()
}
private
fun
parseFile
(
file
:
JSONObject
):
ShareTarget
.
Files
?
{
val
name
=
file
.
tryGetString
(
"name"
)
val
accept
=
file
.
opt
(
"accept"
)
if
(
name
.
isNullOrEmpty
())
return
null
return
ShareTarget
.
Files
(
name
=
name
,
accept
=
when
(
accept
)
{
is
String
->
listOf
(
accept
)
is
JSONArray
->
accept
.
asSequence
{
i
->
getString
(
i
)
}.
toList
()
else
->
emptyList
()
}
)
}
}
components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/WebAppManifestIconParser.kt
→
components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/
parser/
WebAppManifestIconParser.kt
View file @
041ff7da
...
...
@@ -4,13 +4,16 @@
@
file
:
Suppress
(
"TooManyFunctions"
)
package
mozilla.components.concept.engine.manifest
package
mozilla.components.concept.engine.manifest
.parser
import
mozilla.components.concept.engine.manifest.Size
import
mozilla.components.concept.engine.manifest.WebAppManifest
import
mozilla.components.support.ktx.android.org.json.asSequence
import
mozilla.components.support.ktx.android.org.json.tryGet
import
mozilla.components.support.ktx.android.org.json.tryGetString
import
org.json.JSONArray
import
org.json.JSONObject
import
java.util.Locale
private
val
whitespace
=
"\\s+"
.
toRegex
()
...
...
@@ -48,17 +51,19 @@ private fun parseStringSet(set: Any?): Sequence<String>? = when (set) {
}
private
fun
parseIconSizes
(
json
:
JSONObject
):
List
<
Size
>
{
val
sizes
=
parseStringSet
(
json
.
tryGet
(
"sizes"
))
?:
return
emptyList
()
val
sizes
=
parseStringSet
(
json
.
tryGet
(
"sizes"
))
?:
return
emptyList
()
return
sizes
.
mapNotNull
{
Size
.
parse
(
it
)
}.
toList
()
}
private
fun
parsePurposes
(
json
:
JSONObject
):
Set
<
WebAppManifest
.
Icon
.
Purpose
>
{
val
purpose
=
parseStringSet
(
json
.
tryGet
(
"purpose"
))
?:
return
setOf
(
WebAppManifest
.
Icon
.
Purpose
.
ANY
)
val
purpose
=
parseStringSet
(
json
.
tryGet
(
"purpose"
))
?:
return
setOf
(
WebAppManifest
.
Icon
.
Purpose
.
ANY
)
return
purpose
.
mapNotNull
{
when
(
it
.
toLowerCase
())
{
when
(
it
.
toLowerCase
(
Locale
.
ROOT
))
{
"badge"
->
WebAppManifest
.
Icon
.
Purpose
.
BADGE
"maskable"
->
WebAppManifest
.
Icon
.
Purpose
.
MASKABLE
"any"
->
WebAppManifest
.
Icon
.
Purpose
.
ANY
...
...
@@ -68,7 +73,7 @@ private fun parsePurposes(json: JSONObject): Set<WebAppManifest.Icon.Purpose> {
.
toSet
()
}
internal
fun
serializeEnumName
(
name
:
String
)
=
name
.
toLowerCase
().
replace
(
'_'
,
'-'
)
internal
fun
serializeEnumName
(
name
:
String
)
=
name
.
toLowerCase
(
Locale
.
ROOT
).
replace
(
'_'
,
'-'
)
internal
fun
serializeIcons
(
icons
:
List
<
WebAppManifest
.
Icon
>):
JSONArray
{
val
list
=
icons
.
map
{
icon
->
...
...
components/concept/engine/src/test/java/mozilla/components/concept/engine/manifest/WebAppManifestParserTest.kt
View file @
041ff7da
...
...
@@ -299,6 +299,83 @@ class WebAppManifestParserTest {
}
}
@Test
fun
`Parsing
manifest
from
Squoosh`
()
{
val
json
=
loadManifest
(
"squoosh.json"
)
val
result
=
WebAppManifestParser
().
parse
(
json
)
assertTrue
(
result
is
WebAppManifestParser
.
Result
.
Success
)
val
manifest
=
(
result
as
WebAppManifestParser
.
Result
.
Success
).
manifest
assertNotNull
(
manifest
)
assertEquals
(
"Squoosh"
,
manifest
.
name
)
assertEquals
(
"Squoosh"
,
manifest
.
shortName
)
assertEquals
(
"/"
,
manifest
.
startUrl
)
assertEquals
(
WebAppManifest
.
DisplayMode
.
STANDALONE
,
manifest
.
display
)
assertEquals
(
Color
.
WHITE
,
manifest
.
backgroundColor
)
assertEquals
(
WebAppManifest
.
TextDirection
.
AUTO
,
manifest
.
dir
)
assertEquals
(
WebAppManifest
.
Orientation
.
ANY
,
manifest
.
orientation
)
assertNull
(
manifest
.
scope
)
assertEquals
(
rgb
(
247
,
143
,
33
),
manifest
.
themeColor
)
assertEquals
(
1
,
manifest
.
icons
.
size
)
manifest
.
icons
[
0
].
apply
{
assertEquals
(
"/assets/icon-large.png"
,
src
)
assertEquals
(
"image/png"
,
type
)
assertEquals
(
listOf
(
Size
(
1024
,
1024
)),
sizes
)
assertEquals
(
setOf
(
WebAppManifest
.
Icon
.
Purpose
.
ANY
),
purpose
)
}
manifest
.
shareTarget
!!
.
apply
{
assertEquals
(
"/?share-target"
,
action
)
assertEquals
(
WebAppManifest
.
ShareTarget
.
RequestMethod
.
POST
,
method
)
assertEquals
(
WebAppManifest
.
ShareTarget
.
EncodingType
.
MULTIPART
,
encType
)
assertEquals
(
WebAppManifest
.
ShareTarget
.
Params
(
title
=
"title"
,
text
=
"body"
,
url
=
"uri"
,
files
=
listOf
(
WebAppManifest
.
ShareTarget
.
Files
(
name
=
"file"
,
accept
=
listOf
(
"image/*"
)
)
)
),
params
)
}
}
@Test
fun
`Parsing
minimal
manifest
with
share
target`
()
{
val
json
=
loadManifest
(
"minimal_share_target.json"
)
val
result
=
WebAppManifestParser
().
parse
(
json
)
assertTrue
(
result
is
WebAppManifestParser
.
Result
.
Success
)
val
manifest
=
(
result
as
WebAppManifestParser
.
Result
.
Success
).
manifest
assertNotNull
(
manifest
)
assertEquals
(
"Minimal"
,
manifest
.
name
)
assertEquals
(
"/"
,
manifest
.
startUrl
)
manifest
.
shareTarget
!!
.
apply
{
assertEquals
(
"/share-target"
,
action
)
assertEquals
(
WebAppManifest
.
ShareTarget
.
RequestMethod
.
GET
,
method
)
assertEquals
(
WebAppManifest
.
ShareTarget
.
EncodingType
.
URL_ENCODED
,
encType
)
assertEquals
(
WebAppManifest
.
ShareTarget
.
Params
(
files
=
listOf
(
WebAppManifest
.
ShareTarget
.
Files
(
name
=
"file"
,
accept
=
listOf
(
"image/*"
)
)
)
),
params
)
}
}
@Test
fun
`Parsing
invalid
JSON`
()
{
val
json
=
loadManifest
(
"invalid_json.json"
)
...
...
@@ -323,6 +400,62 @@ class WebAppManifestParserTest {
assertTrue
(
result
is
WebAppManifestParser
.
Result
.
Failure
)
}
@Test
fun
`Ignore
missing
share
target
action`
()
{
val
json
=
loadManifest
(
"minimal.json"
).
apply
{
put
(
"share_target"
,
JSONObject
().
apply
{
put
(
"method"
,
"POST"
)
})
}
val
result
=
WebAppManifestParser
().
parse
(
json
)
assertTrue
(
result
is
WebAppManifestParser
.
Result
.
Success
)
assertNull
(
result
.
getOrNull
()
!!
.
shareTarget
)
}
@Test
fun
`Ignore
invalid
share
target
method`
()
{
val
json
=
loadManifest
(
"minimal.json"
).
apply
{
put
(
"share_target"
,
JSONObject
().
apply
{
put
(
"action"
,
"https://mozilla.com/target"
)
put
(
"method"
,
"PATCH"
)
})
}
val
result
=
WebAppManifestParser
().
parse
(
json
)
assertTrue
(
result
is
WebAppManifestParser
.
Result
.
Success
)
assertNull
(
result
.
getOrNull
()
!!
.
shareTarget
)
}
@Test
fun
`Ignore
invalid
share
target
encoding
type`
()
{
val
json
=
loadManifest
(
"minimal.json"
).
apply
{
put
(
"share_target"
,
JSONObject
().
apply
{
put
(
"action"
,
"https://mozilla.com/target"
)
put
(
"enctype"
,
"text/plain"
)
})
}
val
result
=
WebAppManifestParser
().
parse
(
json
)
assertTrue
(
result
is
WebAppManifestParser
.
Result
.
Success
)
assertNull
(
result
.
getOrNull
()
!!
.
shareTarget
)
}
@Test
fun
`Ignore
invalid
share
target
method
and
encoding
type
combo`
()
{
val
json
=
loadManifest
(
"minimal.json"
).
apply
{
put
(
"share_target"
,
JSONObject
().
apply
{
put
(
"action"
,
"https://mozilla.com/target"
)
put
(
"method"
,
"GET"
)
put
(
"enctype"
,
"multipart/form-data"
)
})
}
val
result
=
WebAppManifestParser
().
parse
(
json
)
assertTrue
(
result
is
WebAppManifestParser
.
Result
.
Success
)
assertNull
(
result
.
getOrNull
()
!!
.
shareTarget
)
}
@Test
fun
`Parsing
manifest
with
unusual
values`
()
{
val
json
=
loadManifest
(
"unusual.json"
)
...
...
components/concept/engine/src/test/resources/manifests/minimal_share_target.json
0 → 100644
View file @
041ff7da
{
"name"
:
"Minimal"
,
"start_url"
:
"/"
,
"share_target"
:
{
"action"
:
"/share-target"
,
"params"
:
{
"files"
:
{
"name"
:
"file"
,
"accept"
:
"image/*"
}
}
}
}
components/concept/engine/src/test/resources/manifests/squoosh.json
0 → 100644
View file @
041ff7da
{
"name"
:
"Squoosh"
,
"short_name"
:
"Squoosh"
,
"start_url"
:
"/"
,
"display"
:
"standalone"
,
"orientation"
:
"any"
,
"background_color"
:
"#ffffff"
,
"theme_color"
:
"#f78f21"
,
"icons"
:
[
{
"src"
:
"/assets/icon-large.png"
,
"type"
:
"image/png"
,
"sizes"
:
"1024x1024"
}
],
"share_target"
:
{
"action"
:
"/?share-target"
,
"method"
:
"POST"
,
"enctype"
:
"multipart/form-data"
,
"params"
:
{
"title"
:
"title"
,
"text"
:
"body"
,
"url"
:
"uri"
,
"files"
:
[
{
"name"
:
"file"
,
"accept"
:
[
"image/*"
]
}
]
}
}
}
components/concept/engine/src/test/resources/manifests/unusual.json
View file @
041ff7da
...
...
@@ -19,5 +19,17 @@
"scope"
:
"/"
,
"display"
:
"minimal-ui"
,
"dir"
:
"rtl"
,
"orientation"
:
"portrait"
"orientation"
:
"portrait"
,
"share_target"
:
{
"action"
:
"/"
,
"method"
:
"get"
,
"params"
:
{
"title"
:
"title"
,
"url"
:
"uri"
},
"files"
:
{
"name"
:
"file"
,
"accept"
:
"image/*"
}
}
}
docs/changelog.md
View file @
041ff7da
...
...
@@ -15,6 +15,9 @@ permalink: /changelog/
*
**feature-contextmenu**
*
The "Save Image" context menu item will no longer prompt before downloading the image.
*
**concept-engine**
*
Added
`WebAppManifest.ShareTarget`
data class.
# 16.0.0
*
[
Commits
](
https://github.com/mozilla-mobile/android-components/compare/v15.0.0...v16.0.0
)
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment