Skip to content

Commit 9ffe14c

Browse files
authored
feat(geo): Added support for Geo category (#1502)
* feat(geo): adds Geo category to Amplify (#1450) * feat(geo): adds implementation of Geo category plugin (#1451) * feat(geo): add Maps API (#1452) * feat(geo): add MapLibre adapter for Geo plugin (#1456) * feat(core): register geo category to Amplify and Hub (#1474) * feat(geo): add getMapStyleDescriptor API to Geo (#1475) * chore(geo): fix checkstyle errors * feat(geo): add convenience method for setting map style (#1497) * chore(geo): add alpha version to geo (#1499) * fix(geo): Add JvmStatic to static methods
1 parent 8b0695f commit 9ffe14c

File tree

40 files changed

+2020
-4
lines changed

40 files changed

+2020
-4
lines changed

aws-geo-location/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

aws-geo-location/build.gradle

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
apply plugin: 'com.android.library'
17+
apply plugin: 'kotlin-android'
18+
apply from: rootProject.file("configuration/checkstyle.gradle")
19+
apply from: rootProject.file("configuration/publishing.gradle")
20+
21+
dependencies {
22+
implementation project(':core')
23+
implementation dependency.aws.location
24+
25+
testImplementation project(':testutils')
26+
testImplementation dependency.junit
27+
testImplementation dependency.robolectric
28+
29+
androidTestImplementation project(':testutils')
30+
androidTestImplementation project(':aws-auth-cognito')
31+
androidTestImplementation dependency.androidx.annotation
32+
androidTestImplementation dependency.androidx.test.core
33+
androidTestImplementation dependency.androidx.test.runner
34+
androidTestImplementation dependency.androidx.test.junit
35+
}
36+

aws-geo-location/gradle.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
POM_ARTIFACT_ID=aws-geo-location
2+
POM_NAME=Amplify Framework for Android - Geo
3+
POM_DESCRIPTION=Amplify Framework for Android - Geo Plugin for Amazon Location Service
4+
POM_PACKAGING=aar
5+
6+
VERSION_NAME=0.1.0
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amplifyframework.geo.location
17+
18+
import android.content.Context
19+
import com.amplifyframework.core.Resources
20+
21+
/**
22+
* Utility to load Cognito user credentials from a json document.
23+
* The document must be formatted as following:
24+
*
25+
* <pre>
26+
* {
27+
* "username": "username123",
28+
* "password": "kool@pass22"
29+
* }
30+
* </pre>
31+
*/
32+
object Credentials {
33+
private const val DEFAULT_ID = "credentials" // credentials.json
34+
private var credentials: Pair<String, String>? = null
35+
36+
fun load(context: Context, identifier: String = DEFAULT_ID): Pair<String, String> {
37+
if (credentials == null) {
38+
credentials = synchronized(this) {
39+
val json = Resources.readJsonResource(context, identifier)
40+
val username = json.getString("username")
41+
val password = json.getString("password")
42+
username to password
43+
}
44+
}
45+
return credentials!!
46+
}
47+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amplifyframework.geo.location
17+
18+
import android.content.Context
19+
import androidx.test.core.app.ApplicationProvider
20+
21+
import com.amplifyframework.auth.AuthCategory
22+
import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin
23+
import com.amplifyframework.geo.GeoCategory
24+
import com.amplifyframework.geo.GeoException
25+
import com.amplifyframework.geo.options.GetMapStyleDescriptorOptions
26+
import com.amplifyframework.testutils.sync.SynchronousAuth
27+
import com.amplifyframework.testutils.sync.SynchronousGeo
28+
import com.amplifyframework.testutils.sync.TestCategory
29+
import org.json.JSONObject
30+
31+
import org.junit.Assert.assertNotNull
32+
import org.junit.Assert.assertTrue
33+
import org.junit.Before
34+
import org.junit.Test
35+
36+
/**
37+
* Tests various functionalities related to Maps API in [AWSLocationGeoPlugin].
38+
*/
39+
class MapsApiTest {
40+
private var auth: SynchronousAuth? = null
41+
private var geo: SynchronousGeo? = null
42+
43+
/**
44+
* Set up test categories to be used for testing.
45+
*/
46+
@Before
47+
fun setUp() {
48+
// Auth plugin uses default configuration
49+
val authPlugin = AWSCognitoAuthPlugin()
50+
val authCategory = TestCategory.forPlugin(authPlugin) as AuthCategory
51+
auth = SynchronousAuth.delegatingTo(authCategory)
52+
53+
// Geo plugin uses above auth category to authenticate users
54+
val geoPlugin = AWSLocationGeoPlugin(authProvider = authCategory)
55+
val geoCategory = TestCategory.forPlugin(geoPlugin) as GeoCategory
56+
geo = SynchronousGeo.delegatingTo(geoCategory)
57+
}
58+
59+
/**
60+
* Tests that default map resource's style document can be fetched from
61+
* Amazon Location Service using [AWSLocationGeoPlugin.getMapStyleDescriptor].
62+
*
63+
* Fetched document must follow the specifications for Mapbox Style format.
64+
* Both "layers" and "sources" are critical information required for rendering
65+
* a map, so assert that both fields exist in the document.
66+
*/
67+
@Test
68+
fun styleDescriptorLoadsProperly() {
69+
signInWithCognito()
70+
val style = geo?.getMapStyleDescriptor(GetMapStyleDescriptorOptions.defaults())
71+
assertNotNull(style)
72+
assertNotNull(style?.json)
73+
74+
// assert that style document is aligned with specs
75+
// https://docs.mapbox.com/mapbox-gl-js/style-spec/
76+
val json = JSONObject(style!!.json)
77+
assertTrue(json.has("layers"))
78+
assertTrue(json.has("sources"))
79+
}
80+
81+
/**
82+
* Tests that user must be authenticated in order to fetch map resource from
83+
* Amazon Location Service.
84+
*
85+
* @throws GeoException will be thrown due to service exception.
86+
*/
87+
@Test(expected = GeoException::class)
88+
fun cannotFetchStyleWithoutAuth() {
89+
signOutFromCognito()
90+
// should not be authorized to fetch map resource from Amazon Location Service
91+
geo?.getMapStyleDescriptor(GetMapStyleDescriptorOptions.defaults())
92+
}
93+
94+
private fun signInWithCognito() {
95+
val context = ApplicationProvider.getApplicationContext<Context>()
96+
val (username, password) = Credentials.load(context)
97+
auth?.signIn(username, password)
98+
}
99+
100+
private fun signOutFromCognito() {
101+
auth?.signOut()
102+
}
103+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License").
6+
You may not use this file except in compliance with the License.
7+
A copy of the License is located at
8+
9+
http://aws.amazon.com/apache2.0
10+
11+
or in the "license" file accompanying this file. This file is distributed
12+
on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
13+
express or implied. See the License for the specific language governing
14+
permissions and limitations under the License.
15+
-->
16+
17+
<manifest package="com.amplifyframework.geo.location"
18+
xmlns:android="http://schemas.android.com/apk/res/android" />
19+
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amplifyframework.geo.location
17+
18+
import android.content.Context
19+
20+
import com.amazonaws.auth.AWSCredentialsProvider
21+
import com.amazonaws.services.geo.AmazonLocationClient
22+
import com.amplifyframework.AmplifyException
23+
import com.amplifyframework.auth.AuthCategory
24+
import com.amplifyframework.core.Amplify
25+
import com.amplifyframework.core.Consumer
26+
import com.amplifyframework.geo.GeoCategoryPlugin
27+
import com.amplifyframework.geo.GeoException
28+
import com.amplifyframework.geo.location.configuration.GeoConfiguration
29+
import com.amplifyframework.geo.location.service.AmazonLocationService
30+
import com.amplifyframework.geo.location.service.GeoService
31+
import com.amplifyframework.geo.models.MapStyle
32+
import com.amplifyframework.geo.models.MapStyleDescriptor
33+
import com.amplifyframework.geo.options.GetMapStyleDescriptorOptions
34+
35+
import org.json.JSONObject
36+
import java.util.concurrent.Executors
37+
38+
/**
39+
* A plugin for the Geo category to interact with Amazon Location Service.
40+
*/
41+
class AWSLocationGeoPlugin(
42+
private val userConfiguration: GeoConfiguration? = null, // for programmatically overriding amplifyconfiguration.json
43+
private val authProvider: AuthCategory = Amplify.Auth
44+
): GeoCategoryPlugin<AmazonLocationClient?>() {
45+
companion object {
46+
private const val GEO_PLUGIN_KEY = "awsLocationGeoPlugin"
47+
private const val AUTH_PLUGIN_KEY = "awsCognitoAuthPlugin"
48+
}
49+
50+
private lateinit var configuration: GeoConfiguration
51+
private lateinit var geoService: GeoService<AmazonLocationClient>
52+
53+
private val executor = Executors.newCachedThreadPool()
54+
private val defaultMapName: String by lazy {
55+
configuration.maps!!.default.mapName
56+
}
57+
58+
override fun getPluginKey(): String {
59+
return GEO_PLUGIN_KEY
60+
}
61+
62+
@Throws(AmplifyException::class)
63+
override fun configure(pluginConfiguration: JSONObject, context: Context) {
64+
try {
65+
this.configuration = userConfiguration ?: GeoConfiguration.fromJson(pluginConfiguration).build()
66+
this.geoService = AmazonLocationService(credentialsProvider(), configuration.region)
67+
} catch (error: Exception) {
68+
throw GeoException("Failed to configure AWSLocationGeoPlugin.", error,
69+
"Make sure your amplifyconfiguration.json is valid.")
70+
}
71+
}
72+
73+
override fun getEscapeHatch(): AmazonLocationClient {
74+
return geoService.provider
75+
}
76+
77+
override fun getVersion(): String {
78+
return BuildConfig.VERSION_NAME
79+
}
80+
81+
override fun getAvailableMaps(
82+
onResult: Consumer<Collection<MapStyle>>,
83+
onError: Consumer<GeoException>
84+
) {
85+
try {
86+
onResult.accept(configuration.maps!!.items)
87+
} catch (error: Exception) {
88+
onError.accept(Errors.mapsError(error))
89+
}
90+
}
91+
92+
override fun getDefaultMap(
93+
onResult: Consumer<MapStyle>,
94+
onError: Consumer<GeoException>
95+
) {
96+
try {
97+
onResult.accept(configuration.maps!!.default)
98+
} catch (error: Exception) {
99+
onError.accept(Errors.mapsError(error))
100+
}
101+
}
102+
103+
override fun getMapStyleDescriptor(
104+
onResult: Consumer<MapStyleDescriptor>,
105+
onError: Consumer<GeoException>
106+
) {
107+
val options = GetMapStyleDescriptorOptions.defaults()
108+
getMapStyleDescriptor(options, onResult, onError)
109+
}
110+
111+
override fun getMapStyleDescriptor(
112+
options: GetMapStyleDescriptorOptions,
113+
onResult: Consumer<MapStyleDescriptor>,
114+
onError: Consumer<GeoException>
115+
) {
116+
execute(
117+
{
118+
val mapName = options.mapName ?: defaultMapName
119+
val styleJson = geoService.getStyleJson(mapName)
120+
MapStyleDescriptor(styleJson)
121+
},
122+
Errors::mapsError,
123+
onResult,
124+
onError
125+
)
126+
}
127+
128+
private fun credentialsProvider(): AWSCredentialsProvider {
129+
val authPlugin = authProvider.getPlugin(AUTH_PLUGIN_KEY)
130+
return authPlugin.escapeHatch as AWSCredentialsProvider
131+
}
132+
133+
// Helper method that launches given task on a new worker thread.
134+
private fun <T : Any> execute(
135+
runnableTask: () -> T,
136+
errorTransformer: (Throwable) -> GeoException,
137+
onResult: Consumer<T>,
138+
onError: Consumer<GeoException>
139+
) {
140+
executor.execute {
141+
try {
142+
val result = runnableTask.invoke()
143+
onResult.accept(result)
144+
} catch (error: Throwable) {
145+
val geoException = errorTransformer.invoke(error)
146+
onError.accept(geoException)
147+
}
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)