Merge pull request #87 from yschimke/deeplinks

ISS Map and deeplinks
This commit is contained in:
John O'Reilly 2021-10-29 16:47:19 +01:00 committed by GitHub
commit 14f1ca190c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 162 additions and 21 deletions

View file

@ -41,6 +41,10 @@ android {
dependencies { dependencies {
with(Deps.Android) {
implementation(osmdroidAndroid)
}
with(Deps.AndroidX) { with(Deps.AndroidX) {
implementation(activityCompose) implementation(activityCompose)
} }

View file

@ -1,6 +1,5 @@
package com.surrus.peopleinspace.wear package com.surrus.peopleinspace.wear
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.assertContentDescriptionEquals import androidx.compose.ui.test.assertContentDescriptionEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createComposeRule
@ -8,10 +7,8 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onParent import androidx.compose.ui.test.onParent
import com.surrus.common.remote.Assignment import com.surrus.common.remote.Assignment
import com.surrus.peopleinspace.LocalRotaryEventDispatcher
import com.surrus.peopleinspace.PersonListScreen import com.surrus.peopleinspace.PersonListScreen
import com.surrus.peopleinspace.PersonListTag import com.surrus.peopleinspace.PersonListTag
import com.surrus.peopleinspace.RotaryEventDispatcher
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -35,7 +32,7 @@ class PeopleInSpaceTest {
@Test @Test
fun testPeopleListScreenEmpty() { fun testPeopleListScreenEmpty() {
composeTestRule.setContent { composeTestRule.setContent {
PersonListScreen(personSelected = {}, people = listOf()) PersonListScreen(personSelected = {}, issMapClick = {}, people = listOf())
} }
composeTestRule.onNodeWithText("No people in space!") composeTestRule.onNodeWithText("No people in space!")
@ -45,7 +42,7 @@ class PeopleInSpaceTest {
@Test @Test
fun testPeopleListScreen() { fun testPeopleListScreen() {
composeTestRule.setContent { composeTestRule.setContent {
PersonListScreen(personSelected = {}, people = peopleList) PersonListScreen(personSelected = {}, issMapClick = {}, people = peopleList)
} }
val personListNode = composeTestRule.onNodeWithTag(PersonListTag) val personListNode = composeTestRule.onNodeWithTag(PersonListTag)

View file

@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />
@ -32,6 +33,17 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="peopleinspace.dev"
android:scheme="peopleinspace" />
</intent-filter>
</activity> </activity>
</application> </application>

View file

@ -0,0 +1,80 @@
package com.surrus.peopleinspace
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.viewinterop.AndroidView
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Scaffold
import com.surrus.common.remote.IssPosition
import org.koin.androidx.compose.getViewModel
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import java.io.File
const val ISSPositionMapTag = "ISSPositionMap"
val IssPositionKey = SemanticsPropertyKey<IssPosition>("IssPosition")
var SemanticsPropertyReceiver.observedIssPosition by IssPositionKey
@Composable
fun IssMap() {
val context = LocalContext.current
org.osmdroid.config.Configuration.getInstance().userAgentValue = BuildConfig.APPLICATION_ID
org.osmdroid.config.Configuration.getInstance().tileFileSystemCacheMaxBytes = 50L * 1024 * 1024
org.osmdroid.config.Configuration.getInstance().osmdroidTileCache =
File(context.cacheDir, "osmdroid").also { it.mkdir() }
val peopleInSpaceViewModel = getViewModel<PeopleInSpaceViewModel>()
val issPosition by peopleInSpaceViewModel.issPosition
.collectAsState(IssPosition(0.0, 0.0))
val mapView = remember { MapView(context) }
MaterialTheme {
Scaffold {
AndroidView(
factory = {
mapView.apply {
setTileSource(TileSourceFactory.MAPNIK);
zoomController.setVisibility(CustomZoomButtonsController.Visibility.SHOW_AND_FADEOUT)
setMultiTouchControls(false)
controller.setZoom(3.0)
// disable movement
setClickable(false)
isFocusable = false
setOnTouchListener { v, event -> true }
}
},
modifier = Modifier
.fillMaxHeight()
.testTag(ISSPositionMapTag)
.semantics { observedIssPosition = issPosition },
update = { map ->
println(issPosition)
val issPositionPoint = GeoPoint(issPosition.latitude, issPosition.longitude)
map.controller.setCenter(issPositionPoint)
map.overlays.clear()
val stationMarker = Marker(map)
stationMarker.position = issPositionPoint
stationMarker.title = "ISS"
map.overlays.add(stationMarker)
}
)
}
}
}

View file

@ -5,6 +5,10 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.navigation.NavDeepLink
import androidx.navigation.NavType
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import androidx.wear.compose.material.ExperimentalWearMaterialApi import androidx.wear.compose.material.ExperimentalWearMaterialApi
import androidx.wear.compose.navigation.SwipeDismissableNavHost import androidx.wear.compose.navigation.SwipeDismissableNavHost
import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.composable
@ -16,9 +20,11 @@ import org.koin.android.ext.android.inject
sealed class Screen(val route: String) { sealed class Screen(val route: String) {
object PersonList : Screen("personList") object PersonList : Screen("personList")
object PersonDetails : Screen("personDetails") object PersonDetails : Screen("personDetails")
object IssMap : Screen("issMap")
} }
const val PERSON_NAME_NAV_ARGUMENT = "personName" const val PERSON_NAME_NAV_ARGUMENT = "personName"
const val DEEPLINK_URI = "peopleinspace://peopleinspace.dev/"
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val imageLoader: ImageLoader by inject() private val imageLoader: ImageLoader by inject()
@ -43,20 +49,39 @@ class MainActivity : ComponentActivity() {
startDestination = Screen.PersonList.route startDestination = Screen.PersonList.route
) { ) {
composable(Screen.PersonList.route) { composable(
PersonListScreen(personSelected = { route = Screen.PersonList.route,
navController.navigate(Screen.PersonDetails.route + "/${it.name}") deepLinks = listOf(NavDeepLink(DEEPLINK_URI + "personList"))
}) ) {
PersonListScreen(
personSelected = {
navController.navigate(Screen.PersonDetails.route + "/${it.name}")
},
issMapClick = {
navController.navigate(Screen.IssMap.route)
})
} }
composable( composable(
route = Screen.PersonDetails.route + "/{$PERSON_NAME_NAV_ARGUMENT}" route = Screen.PersonDetails.route + "/{$PERSON_NAME_NAV_ARGUMENT}",
arguments = listOf(
navArgument(PERSON_NAME_NAV_ARGUMENT, builder = {
this.type = NavType.StringType
})),
deepLinks = listOf(navDeepLink { uriPattern = DEEPLINK_URI + "personList/{${PERSON_NAME_NAV_ARGUMENT}}" })
) { navBackStackEntry -> ) { navBackStackEntry ->
val personName = navBackStackEntry.arguments?.getString(PERSON_NAME_NAV_ARGUMENT) val personName = navBackStackEntry.arguments?.getString(PERSON_NAME_NAV_ARGUMENT)
personName?.let { personName?.let {
PersonDetailsScreen(personName) PersonDetailsScreen(personName)
} }
} }
composable(
route = Screen.IssMap.route,
deepLinks = listOf(NavDeepLink(DEEPLINK_URI + "issMap"))
) {
IssMap()
}
} }
} }
} }

View file

@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@ -13,20 +14,25 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.Card import androidx.wear.compose.material.Card
import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.ScalingLazyColumn import androidx.wear.compose.material.ScalingLazyColumn
@ -45,12 +51,13 @@ const val NoPeopleTag = "NoPeople"
@Composable @Composable
fun PersonListScreen( fun PersonListScreen(
personSelected: (person: Assignment) -> Unit, personSelected: (person: Assignment) -> Unit,
issMapClick: () -> Unit,
peopleInSpaceViewModel: PeopleInSpaceViewModel = getViewModel() peopleInSpaceViewModel: PeopleInSpaceViewModel = getViewModel()
) { ) {
val peopleState by peopleInSpaceViewModel.peopleInSpace.collectAsState() val peopleState by peopleInSpaceViewModel.peopleInSpace.collectAsState()
val scrollState = rememberScalingLazyListState() val scrollState = rememberScalingLazyListState()
RotaryEventState(scrollState) RotaryEventState(scrollState)
PersonListScreen(peopleState, personSelected, scrollState) PersonListScreen(peopleState, personSelected, issMapClick, scrollState)
} }
@OptIn(ExperimentalAnimationApi::class) @OptIn(ExperimentalAnimationApi::class)
@ -58,6 +65,7 @@ fun PersonListScreen(
fun PersonListScreen( fun PersonListScreen(
people: List<Assignment>?, people: List<Assignment>?,
personSelected: (person: Assignment) -> Unit, personSelected: (person: Assignment) -> Unit,
issMapClick: () -> Unit,
scrollState: ScalingLazyListState = rememberScalingLazyListState(), scrollState: ScalingLazyListState = rememberScalingLazyListState(),
) { ) {
MaterialTheme { MaterialTheme {
@ -67,7 +75,7 @@ fun PersonListScreen(
) { ) {
if (people != null) { if (people != null) {
if (people.isNotEmpty()) { if (people.isNotEmpty()) {
PersonList(people, personSelected, scrollState) PersonList(people, personSelected, issMapClick, scrollState)
} else { } else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Card( Card(
@ -87,19 +95,29 @@ fun PersonListScreen(
fun PersonList( fun PersonList(
people: List<Assignment>, people: List<Assignment>,
personSelected: (person: Assignment) -> Unit, personSelected: (person: Assignment) -> Unit,
issMapClick: () -> Unit,
scrollState: ScalingLazyListState = rememberScalingLazyListState(), scrollState: ScalingLazyListState = rememberScalingLazyListState(),
) { ) {
val paddingHeight = if (LocalConfiguration.current.isScreenRound) 50.dp else 8.dp
ScalingLazyColumn( ScalingLazyColumn(
contentPadding = PaddingValues( contentPadding = PaddingValues(8.dp),
start = 8.dp,
end = 8.dp,
top = paddingHeight,
bottom = paddingHeight
),
modifier = Modifier.testTag(PersonListTag), modifier = Modifier.testTag(PersonListTag),
state = scrollState, state = scrollState,
) { ) {
item {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Button(
modifier = Modifier.size(ButtonDefaults.SmallButtonSize).wrapContentSize(),
onClick = issMapClick
) {
// https://www.svgrepo.com/svg/170716/international-space-station
Image(
modifier = Modifier.scale(0.5f),
painter = painterResource(id = R.drawable.ic_iss),
contentDescription = "ISS Map"
)
}
}
}
items(people.size) { offset -> items(people.size) { offset ->
PersonView(people[offset], personSelected) PersonView(people[offset], personSelected)
} }
@ -197,7 +215,7 @@ fun PersonListSquarePreview() {
"Buzz Aldrin", "Buzz Aldrin",
"https://nypost.com/wp-content/uploads/sites/2/2018/06/buzz-aldrin.jpg?quality=80&strip=all" "https://nypost.com/wp-content/uploads/sites/2/2018/06/buzz-aldrin.jpg?quality=80&strip=all"
) )
), personSelected = {}) ), personSelected = {}, issMapClick = {})
} }
@Preview( @Preview(
@ -210,5 +228,5 @@ fun PersonListSquarePreview() {
) )
@Composable @Composable
fun PersonListSquareEmptyPreview() { fun PersonListSquareEmptyPreview() {
PersonListScreen(people = listOf(), personSelected = {}) PersonListScreen(people = listOf(), personSelected = {}, issMapClick = {})
} }

View file

@ -0,0 +1,5 @@
<vector android:height="64dp" android:viewportHeight="401.294"
android:viewportWidth="401.294" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M115.342,188.015c-7.025,0 -12.741,5.716 -12.741,12.741s5.716,12.741 12.741,12.741c7.026,0 12.742,-5.716 12.742,-12.741S122.368,188.015 115.342,188.015zM115.342,205.497c-2.614,0 -4.741,-2.127 -4.741,-4.741s2.127,-4.741 4.741,-4.741c2.615,0 4.742,2.127 4.742,4.741S117.957,205.497 115.342,205.497z"/>
<path android:fillColor="#FF000000" android:pathData="M397.294,164.141c2.209,0 4,-1.791 4,-4V89.215c0,-2.209 -1.791,-4 -4,-4h-33.219c-2.209,0 -4,1.791 -4,4v70.926c0,2.209 1.791,4 4,4h12.61v8.999h-36.205c-6.008,-4.585 -13.401,-7.094 -20.986,-7.094c-17.762,0 -32.434,13.455 -34.376,30.706h-3.421v-9.79c0,-2.209 -1.791,-4 -4,-4h-28.993l-6.173,-11.125c-0.705,-1.271 -2.044,-2.059 -3.498,-2.059h-39.513v-6.524h16.196c2.209,0 4,-1.791 4,-4V73.281c0,-2.209 -1.791,-4 -4,-4h-40.266c-2.209,0 -4,1.791 -4,4v85.972c0,2.209 1.791,4 4,4h16.069v6.524h-38.468c-2.209,0 -4,1.791 -4,4v22.974h-4.52c-1.97,-15.047 -14.865,-26.705 -30.44,-26.705c-10.313,0 -19.703,5.125 -25.326,13.331c-5.623,-8.206 -15.012,-13.331 -25.326,-13.331H44.653v-5.905h6.324c2.209,0 4,-1.791 4,-4V89.215c0,-2.209 -1.791,-4 -4,-4H17.758c-2.209,0 -4,1.791 -4,4v70.926c0,2.209 1.791,4 4,4h6.325v5.905H4c-2.209,0 -4,1.791 -4,4v53.419c0,2.209 1.791,4 4,4h20.083v5.905h-6.325c-2.209,0 -4,1.791 -4,4v70.927c0,2.209 1.791,4 4,4h33.219c2.209,0 4,-1.791 4,-4v-70.927c0,-2.209 -1.791,-4 -4,-4h-6.325v-5.905h18.789c10.313,0 19.703,-5.125 25.326,-13.331c5.624,8.205 15.013,13.331 25.326,13.331c15.578,0 28.476,-11.663 30.441,-26.714h4.518v22.765c0,2.209 1.791,4 4,4h38.468v6.524h-16.069c-2.209,0 -4,1.791 -4,4v85.972c0,2.209 1.791,4 4,4h40.266c2.209,0 4,-1.791 4,-4V242.04c0,-2.209 -1.791,-4 -4,-4h-16.196v-6.524h39.513c1.454,0 2.792,-0.789 3.498,-2.059l6.173,-11.125h28.993c2.209,0 4,-1.791 4,-4v-9.581h3.447c2.037,17.151 16.66,30.497 34.35,30.497c7.581,0 14.972,-2.507 20.978,-7.087h36.214v9.209h-12.61c-2.209,0 -4,1.791 -4,4v70.927c0,2.209 1.791,4 4,4h33.219c2.209,0 4,-1.791 4,-4v-70.927c0,-2.209 -1.791,-4 -4,-4h-12.609v-9.209h12.435c2.209,0 4,-1.791 4,-4V177.14c0,-2.209 -1.791,-4 -4,-4h-12.435v-8.999H397.294zM199.521,155.253v-13.221h12.196v13.221H199.521zM191.521,112.267h-12.069V98.502h12.069V112.267zM199.521,98.502h12.196v13.765h-12.196V98.502zM191.521,120.267v13.765h-12.069v-13.765H191.521zM199.521,120.267h12.196v13.765h-12.196V120.267zM211.717,90.502h-12.196V77.281h12.196V90.502zM191.521,77.281v13.221h-12.069V77.281H191.521zM179.451,155.253v-13.221h12.069v13.221H179.451zM24.689,223.466v-45.419h22.038v45.419H24.689zM54.727,178.046h6.641v45.419h-6.641V178.046zM46.977,128.678v9.956H21.758v-9.956H46.977zM21.758,120.678v-9.957h25.219v9.957H21.758zM46.977,93.215v9.507H21.758v-9.507H46.977zM21.758,146.634h25.219v9.507H21.758V146.634zM32.083,164.141h4.569v5.905h-4.569V164.141zM8,178.046h8.689v45.419H8V178.046zM21.758,272.834v-9.956h25.219v9.956H21.758zM46.977,280.834v9.956H21.758v-9.956H46.977zM21.758,308.298v-9.507h25.219v9.507H21.758zM46.977,254.878H21.758v-9.507h25.219V254.878zM36.652,237.371h-4.569v-5.905h4.569V237.371zM69.368,222.667v-43.822c6.952,1.876 12.703,6.983 15.31,13.918v15.987C82.071,215.684 76.32,220.791 69.368,222.667zM114.093,223.466c-9.664,0 -18.222,-6.091 -21.415,-15.184v-15.05c3.193,-9.094 11.751,-15.185 21.415,-15.185c12.522,0 22.709,10.188 22.709,22.71C136.802,213.278 126.615,223.466 114.093,223.466zM191.521,246.04v13.221h-12.069V246.04H191.521zM199.521,289.027h12.196v13.765h-12.196V289.027zM191.521,302.792h-12.069v-13.765h12.069V302.792zM199.521,281.027v-13.765h12.196v13.765H199.521zM191.521,281.027h-12.069v-13.765h12.069V281.027zM179.451,310.792h12.069v13.221h-12.069V310.792zM199.521,324.012v-13.221h12.196v13.221H199.521zM211.717,246.04v13.221h-12.196V246.04H211.717zM157.052,177.778h11.046v45.739h-11.046V177.778zM236.678,223.516h-60.58v-45.739h60.58l5.311,9.571v26.596L236.678,223.516zM273.696,210.332h-23.707v-19.371h23.707V210.332zM368.075,272.834v-9.956h25.219v9.956H368.075zM393.294,280.834v9.956h-25.219v-9.956H393.294zM368.075,308.298v-9.507h25.219v9.507H368.075zM393.294,254.878h-25.219v-9.507h25.219V254.878zM292.892,200.647c0,-8.817 4.315,-16.641 10.94,-21.484v42.968C297.207,217.288 292.892,209.463 292.892,200.647zM319.493,227.248c-2.663,0 -5.234,-0.398 -7.661,-1.129v-50.944c2.428,-0.731 4.998,-1.129 7.661,-1.129c6.217,0 12.267,2.193 17.036,6.175c0.248,0.208 0.521,0.368 0.803,0.506v39.849c-0.29,0.141 -0.57,0.304 -0.821,0.513C331.747,225.061 325.703,227.248 319.493,227.248zM393.119,220.161h-47.787V181.14h47.787V220.161zM393.294,128.678v9.956h-25.219v-9.956H393.294zM368.075,120.678v-9.957h25.219v9.957H368.075zM393.294,93.215v9.507h-25.219v-9.507H393.294zM368.075,146.634h25.219v9.507h-25.219V146.634z"/>
</vector>