diff --git a/wearApp/build.gradle.kts b/wearApp/build.gradle.kts index 4668e66..05eb325 100644 --- a/wearApp/build.gradle.kts +++ b/wearApp/build.gradle.kts @@ -41,6 +41,10 @@ android { dependencies { + with(Deps.Android) { + implementation(osmdroidAndroid) + } + with(Deps.AndroidX) { implementation(activityCompose) } diff --git a/wearApp/src/androidTest/java/com/surrus/peopleinspace/wear/PeopleInSpaceTest.kt b/wearApp/src/androidTest/java/com/surrus/peopleinspace/wear/PeopleInSpaceTest.kt index fde15eb..907d56a 100644 --- a/wearApp/src/androidTest/java/com/surrus/peopleinspace/wear/PeopleInSpaceTest.kt +++ b/wearApp/src/androidTest/java/com/surrus/peopleinspace/wear/PeopleInSpaceTest.kt @@ -1,6 +1,5 @@ package com.surrus.peopleinspace.wear -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.assertContentDescriptionEquals import androidx.compose.ui.test.assertIsDisplayed 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.onParent import com.surrus.common.remote.Assignment -import com.surrus.peopleinspace.LocalRotaryEventDispatcher import com.surrus.peopleinspace.PersonListScreen import com.surrus.peopleinspace.PersonListTag -import com.surrus.peopleinspace.RotaryEventDispatcher import org.junit.Rule import org.junit.Test @@ -35,7 +32,7 @@ class PeopleInSpaceTest { @Test fun testPeopleListScreenEmpty() { composeTestRule.setContent { - PersonListScreen(personSelected = {}, people = listOf()) + PersonListScreen(personSelected = {}, issMapClick = {}, people = listOf()) } composeTestRule.onNodeWithText("No people in space!") @@ -45,7 +42,7 @@ class PeopleInSpaceTest { @Test fun testPeopleListScreen() { composeTestRule.setContent { - PersonListScreen(personSelected = {}, people = peopleList) + PersonListScreen(personSelected = {}, issMapClick = {}, people = peopleList) } val personListNode = composeTestRule.onNodeWithTag(PersonListTag) diff --git a/wearApp/src/main/AndroidManifest.xml b/wearApp/src/main/AndroidManifest.xml index 9db7585..c99c37e 100644 --- a/wearApp/src/main/AndroidManifest.xml +++ b/wearApp/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + @@ -32,6 +33,17 @@ + + + + + + + + + diff --git a/wearApp/src/main/java/com/surrus/peopleinspace/IssMap.kt b/wearApp/src/main/java/com/surrus/peopleinspace/IssMap.kt new file mode 100644 index 0000000..371e715 --- /dev/null +++ b/wearApp/src/main/java/com/surrus/peopleinspace/IssMap.kt @@ -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") +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() + + 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) + } + ) + } + } +} \ No newline at end of file diff --git a/wearApp/src/main/java/com/surrus/peopleinspace/MainActivity.kt b/wearApp/src/main/java/com/surrus/peopleinspace/MainActivity.kt index 2be05ce..897cc03 100644 --- a/wearApp/src/main/java/com/surrus/peopleinspace/MainActivity.kt +++ b/wearApp/src/main/java/com/surrus/peopleinspace/MainActivity.kt @@ -5,6 +5,10 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi 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.navigation.SwipeDismissableNavHost import androidx.wear.compose.navigation.composable @@ -16,9 +20,11 @@ import org.koin.android.ext.android.inject sealed class Screen(val route: String) { object PersonList : Screen("personList") object PersonDetails : Screen("personDetails") + object IssMap : Screen("issMap") } const val PERSON_NAME_NAV_ARGUMENT = "personName" +const val DEEPLINK_URI = "peopleinspace://peopleinspace.dev/" class MainActivity : ComponentActivity() { private val imageLoader: ImageLoader by inject() @@ -43,20 +49,39 @@ class MainActivity : ComponentActivity() { startDestination = Screen.PersonList.route ) { - composable(Screen.PersonList.route) { - PersonListScreen(personSelected = { - navController.navigate(Screen.PersonDetails.route + "/${it.name}") - }) + composable( + route = Screen.PersonList.route, + deepLinks = listOf(NavDeepLink(DEEPLINK_URI + "personList")) + ) { + PersonListScreen( + personSelected = { + navController.navigate(Screen.PersonDetails.route + "/${it.name}") + }, + issMapClick = { + navController.navigate(Screen.IssMap.route) + }) } 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 -> val personName = navBackStackEntry.arguments?.getString(PERSON_NAME_NAV_ARGUMENT) personName?.let { PersonDetailsScreen(personName) } } + + composable( + route = Screen.IssMap.route, + deepLinks = listOf(NavDeepLink(DEEPLINK_URI + "issMap")) + ) { + IssMap() + } } } } diff --git a/wearApp/src/main/java/com/surrus/peopleinspace/PersonListScreen.kt b/wearApp/src/main/java/com/surrus/peopleinspace/PersonListScreen.kt index 8693803..1005a2a 100644 --- a/wearApp/src/main/java/com/surrus/peopleinspace/PersonListScreen.kt +++ b/wearApp/src/main/java/com/surrus/peopleinspace/PersonListScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.slideInVertically import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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.fillMaxWidth import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview 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.MaterialTheme import androidx.wear.compose.material.ScalingLazyColumn @@ -45,12 +51,13 @@ const val NoPeopleTag = "NoPeople" @Composable fun PersonListScreen( personSelected: (person: Assignment) -> Unit, + issMapClick: () -> Unit, peopleInSpaceViewModel: PeopleInSpaceViewModel = getViewModel() ) { val peopleState by peopleInSpaceViewModel.peopleInSpace.collectAsState() val scrollState = rememberScalingLazyListState() RotaryEventState(scrollState) - PersonListScreen(peopleState, personSelected, scrollState) + PersonListScreen(peopleState, personSelected, issMapClick, scrollState) } @OptIn(ExperimentalAnimationApi::class) @@ -58,6 +65,7 @@ fun PersonListScreen( fun PersonListScreen( people: List?, personSelected: (person: Assignment) -> Unit, + issMapClick: () -> Unit, scrollState: ScalingLazyListState = rememberScalingLazyListState(), ) { MaterialTheme { @@ -67,7 +75,7 @@ fun PersonListScreen( ) { if (people != null) { if (people.isNotEmpty()) { - PersonList(people, personSelected, scrollState) + PersonList(people, personSelected, issMapClick, scrollState) } else { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Card( @@ -87,19 +95,29 @@ fun PersonListScreen( fun PersonList( people: List, personSelected: (person: Assignment) -> Unit, + issMapClick: () -> Unit, scrollState: ScalingLazyListState = rememberScalingLazyListState(), ) { - val paddingHeight = if (LocalConfiguration.current.isScreenRound) 50.dp else 8.dp ScalingLazyColumn( - contentPadding = PaddingValues( - start = 8.dp, - end = 8.dp, - top = paddingHeight, - bottom = paddingHeight - ), + contentPadding = PaddingValues(8.dp), modifier = Modifier.testTag(PersonListTag), 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 -> PersonView(people[offset], personSelected) } @@ -197,7 +215,7 @@ fun PersonListSquarePreview() { "Buzz Aldrin", "https://nypost.com/wp-content/uploads/sites/2/2018/06/buzz-aldrin.jpg?quality=80&strip=all" ) - ), personSelected = {}) + ), personSelected = {}, issMapClick = {}) } @Preview( @@ -210,5 +228,5 @@ fun PersonListSquarePreview() { ) @Composable fun PersonListSquareEmptyPreview() { - PersonListScreen(people = listOf(), personSelected = {}) + PersonListScreen(people = listOf(), personSelected = {}, issMapClick = {}) } diff --git a/wearApp/src/main/res/drawable/ic_iss.xml b/wearApp/src/main/res/drawable/ic_iss.xml new file mode 100644 index 0000000..cdd0d01 --- /dev/null +++ b/wearApp/src/main/res/drawable/ic_iss.xml @@ -0,0 +1,5 @@ + + + +