Skip to content

Commit 6acedf0

Browse files
committed
[Push Notifications Revamp] Support deeplink for Staff Picks (#3872)
1 parent 61d2494 commit 6acedf0

File tree

8 files changed

+257
-0
lines changed

8 files changed

+257
-0
lines changed

app/src/androidTest/java/au/com/shiftyjelly/pocketcasts/deeplink/DeepLinkFactoryTest.kt

+11
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,17 @@ class DeepLinkFactoryTest {
594594
assertEquals(ImportDeepLink, deepLink)
595595
}
596596

597+
@Test
598+
fun staffPicks() {
599+
val intent = Intent()
600+
.setAction(ACTION_VIEW)
601+
.setData(Uri.parse("pktc://discover/staffpicks"))
602+
603+
val deepLink = factory.create(intent)
604+
605+
assertEquals(StaffPicksDeepLink, deepLink)
606+
}
607+
597608
@Test
598609
fun nativeShare() {
599610
val intent = Intent()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package au.com.shiftyjelly.pocketcasts.deeplink
2+
3+
import android.content.Intent.ACTION_VIEW
4+
import android.net.Uri
5+
import androidx.test.ext.junit.runners.AndroidJUnit4
6+
import androidx.test.platform.app.InstrumentationRegistry
7+
import org.junit.Assert.assertEquals
8+
import org.junit.Test
9+
import org.junit.runner.RunWith
10+
11+
@RunWith(AndroidJUnit4::class)
12+
class StaffPicksDeepLinkTest {
13+
private val context = InstrumentationRegistry.getInstrumentation().context
14+
15+
@Test
16+
fun createIntent() {
17+
val intent = StaffPicksDeepLink.toIntent(context)
18+
19+
assertEquals(ACTION_VIEW, intent.action)
20+
assertEquals(Uri.parse("pktc://discover/staffpicks"), intent.data)
21+
}
22+
}

app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt

+17
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,14 @@ import au.com.shiftyjelly.pocketcasts.deeplink.ShowUpNextModalDeepLink
7878
import au.com.shiftyjelly.pocketcasts.deeplink.ShowUpNextTabDeepLink
7979
import au.com.shiftyjelly.pocketcasts.deeplink.SignInDeepLink
8080
import au.com.shiftyjelly.pocketcasts.deeplink.SonosDeepLink
81+
import au.com.shiftyjelly.pocketcasts.deeplink.StaffPicksDeepLink
8182
import au.com.shiftyjelly.pocketcasts.deeplink.ThemesDeepLink
8283
import au.com.shiftyjelly.pocketcasts.deeplink.UpgradeAccountDeepLink
8384
import au.com.shiftyjelly.pocketcasts.deeplink.UpsellDeepLink
85+
import au.com.shiftyjelly.pocketcasts.discover.util.DiscoverDeepLinkManager
86+
import au.com.shiftyjelly.pocketcasts.discover.util.DiscoverDeepLinkManager.Companion.STAFF_PICKS_LIST_ID
8487
import au.com.shiftyjelly.pocketcasts.discover.view.DiscoverFragment
88+
import au.com.shiftyjelly.pocketcasts.discover.view.PodcastListFragment
8589
import au.com.shiftyjelly.pocketcasts.endofyear.StoriesActivity
8690
import au.com.shiftyjelly.pocketcasts.endofyear.StoriesActivity.StoriesSource
8791
import au.com.shiftyjelly.pocketcasts.endofyear.ui.EndOfYearLaunchBottomSheet
@@ -245,6 +249,8 @@ class MainActivity :
245249

246250
@Inject lateinit var notificationHelper: NotificationHelper
247251

252+
@Inject lateinit var discoverDeepLinkManager: DiscoverDeepLinkManager
253+
248254
@Inject @ApplicationScope
249255
lateinit var applicationScope: CoroutineScope
250256

@@ -1352,6 +1358,17 @@ class MainActivity :
13521358
is ImportDeepLink -> {
13531359
openImport()
13541360
}
1361+
is StaffPicksDeepLink -> {
1362+
val podcastListFragment = supportFragmentManager.fragments.find { it is PodcastListFragment } as? PodcastListFragment
1363+
if (podcastListFragment?.listUuid != STAFF_PICKS_LIST_ID) {
1364+
openTab(VR.id.navigation_discover)
1365+
lifecycleScope.launch {
1366+
val staffPicksList = discoverDeepLinkManager.getDiscoverList(STAFF_PICKS_LIST_ID, resources) ?: return@launch
1367+
val fragment = PodcastListFragment.newInstance(staffPicksList)
1368+
addFragment(fragment)
1369+
}
1370+
}
1371+
}
13551372
is PlayFromSearchDeepLink -> {
13561373
playbackManager.mediaSessionManager.playFromSearchExternal(deepLink.query)
13571374
}

modules/features/discover/build.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,6 @@ dependencies {
6565
testImplementation(libs.okhttp)
6666
testImplementation(libs.retrofit)
6767
testImplementation(libs.retrofit.moshi)
68+
testImplementation(libs.mockito.kotlin)
69+
testImplementation(projects.modules.services.sharedtest)
6870
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package au.com.shiftyjelly.pocketcasts.discover.util
2+
3+
import android.content.res.Resources
4+
import au.com.shiftyjelly.pocketcasts.preferences.Settings
5+
import au.com.shiftyjelly.pocketcasts.servers.model.Discover
6+
import au.com.shiftyjelly.pocketcasts.servers.model.DiscoverRow
7+
import au.com.shiftyjelly.pocketcasts.servers.model.NetworkLoadableList
8+
import au.com.shiftyjelly.pocketcasts.servers.model.transformWithRegion
9+
import au.com.shiftyjelly.pocketcasts.servers.server.ListRepository
10+
import javax.inject.Inject
11+
import kotlinx.coroutines.Dispatchers
12+
import kotlinx.coroutines.withContext
13+
14+
class DiscoverDeepLinkManager @Inject constructor(
15+
private val repository: ListRepository,
16+
private val settings: Settings,
17+
) {
18+
companion object {
19+
const val STAFF_PICKS_LIST_ID = "staff-picks"
20+
}
21+
22+
suspend fun getDiscoverList(listId: String, resources: Resources): NetworkLoadableList? = withContext(Dispatchers.IO) {
23+
val discover: Discover = repository.getDiscoverFeedSuspend()
24+
val currentRegionCode: String = settings.discoverCountryCode.value
25+
val defaultRegion: String = discover.defaultRegionCode
26+
val region = discover.regions[currentRegionCode] ?: discover.regions[defaultRegion] ?: return@withContext null
27+
28+
val replacements: Map<String, String> = mapOf(
29+
discover.regionCodeToken to region.code,
30+
discover.regionNameToken to region.name,
31+
)
32+
33+
val discoverRows: List<DiscoverRow> = discover.layout.transformWithRegion(region, replacements, resources)
34+
val staffPicksRow: DiscoverRow? = discoverRows.firstOrNull { it.listUuid == listId }
35+
staffPicksRow?.transformWithReplacements(replacements, resources)
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package au.com.shiftyjelly.pocketcasts.discover.util
2+
3+
import android.content.res.Resources
4+
import au.com.shiftyjelly.pocketcasts.preferences.Settings
5+
import au.com.shiftyjelly.pocketcasts.preferences.UserSetting
6+
import au.com.shiftyjelly.pocketcasts.servers.model.Discover
7+
import au.com.shiftyjelly.pocketcasts.servers.model.DiscoverRegion
8+
import au.com.shiftyjelly.pocketcasts.servers.model.DiscoverRow
9+
import au.com.shiftyjelly.pocketcasts.servers.model.DisplayStyle
10+
import au.com.shiftyjelly.pocketcasts.servers.model.ExpandedStyle
11+
import au.com.shiftyjelly.pocketcasts.servers.model.ListType
12+
import au.com.shiftyjelly.pocketcasts.servers.model.SponsoredPodcast
13+
import au.com.shiftyjelly.pocketcasts.servers.server.ListRepository
14+
import au.com.shiftyjelly.pocketcasts.sharedtest.MainCoroutineRule
15+
import kotlinx.coroutines.ExperimentalCoroutinesApi
16+
import kotlinx.coroutines.flow.MutableStateFlow
17+
import kotlinx.coroutines.test.runTest
18+
import org.junit.Before
19+
import org.junit.Rule
20+
import org.junit.Test
21+
import org.mockito.Mock
22+
import org.mockito.MockitoAnnotations
23+
import org.mockito.kotlin.any
24+
import org.mockito.kotlin.mock
25+
import org.mockito.kotlin.whenever
26+
27+
@ExperimentalCoroutinesApi
28+
class DiscoverDeepLinkManagerTest {
29+
30+
@get:Rule
31+
val coroutineRule = MainCoroutineRule()
32+
33+
@Mock
34+
private lateinit var mockRepository: ListRepository
35+
36+
@Mock
37+
private lateinit var mockSettings: Settings
38+
39+
@Mock
40+
private lateinit var mockResources: Resources
41+
42+
private lateinit var manager: DiscoverDeepLinkManager
43+
44+
@Before
45+
fun setUp() {
46+
MockitoAnnotations.openMocks(this)
47+
manager = DiscoverDeepLinkManager(mockRepository, mockSettings)
48+
}
49+
50+
@Test
51+
fun `loadStaffPicks should return transformed staff picks row`() = runTest {
52+
val staffPicksRow = createDiscoverRow(
53+
listUuid = "staff-picks",
54+
title = "Popular in {region_name}",
55+
source = "source_{region_code}",
56+
expandedTopItemLabel = "Top in {region_name}",
57+
)
58+
val discover = createTestDiscover(layout = listOf(staffPicksRow))
59+
60+
whenever(mockRepository.getDiscoverFeedSuspend()).thenReturn(discover)
61+
62+
val discoverCountryCodeMock: UserSetting<String> = mock()
63+
whenever(discoverCountryCodeMock.flow).thenReturn(MutableStateFlow("US"))
64+
whenever(mockSettings.discoverCountryCode).thenReturn(discoverCountryCodeMock)
65+
66+
whenever(mockResources.getString(any())).thenAnswer { it.arguments[0].toString() }
67+
68+
val result = manager.getDiscoverList("staff-picks", mockResources)
69+
70+
assert(result != null)
71+
assert(result?.listUuid == "staff-picks")
72+
}
73+
74+
@Test
75+
fun `loadStaffPicks should return null when region not in row regions`() = runTest {
76+
val row = createDiscoverRow(
77+
listUuid = "staff-picks",
78+
regions = listOf("JP"),
79+
)
80+
val discover = createTestDiscover(layout = listOf(row))
81+
82+
whenever(mockRepository.getDiscoverFeedSuspend()).thenReturn(discover)
83+
84+
val discoverCountryCodeMock: UserSetting<String> = mock()
85+
whenever(discoverCountryCodeMock.flow).thenReturn(MutableStateFlow("US"))
86+
whenever(mockSettings.discoverCountryCode).thenReturn(discoverCountryCodeMock)
87+
88+
val result = manager.getDiscoverList("staff-picks", mockResources)
89+
90+
assert(result == null)
91+
}
92+
93+
private fun createTestDiscover(
94+
layout: List<DiscoverRow> = listOf(createDiscoverRow(listUuid = "staff-picks")),
95+
regions: Map<String, DiscoverRegion> = mapOf(
96+
"US" to DiscoverRegion("United States", "US-flag", "US"),
97+
"UK" to DiscoverRegion("United Kingdom", "UK-flag", "UK"),
98+
),
99+
regionCodeToken: String = "{region_code}",
100+
regionNameToken: String = "{region_name}",
101+
defaultRegionCode: String = "US",
102+
): Discover {
103+
return Discover(
104+
layout = layout,
105+
regions = regions,
106+
regionCodeToken = regionCodeToken,
107+
regionNameToken = regionNameToken,
108+
defaultRegionCode = defaultRegionCode,
109+
)
110+
}
111+
112+
private fun createDiscoverRow(
113+
id: String? = null,
114+
type: ListType = ListType.PodcastList,
115+
displayStyle: DisplayStyle = DisplayStyle.SmallList(),
116+
expandedStyle: ExpandedStyle = ExpandedStyle.GridList(),
117+
expandedTopItemLabel: String? = null,
118+
title: String = "Title",
119+
source: String = "Source",
120+
listUuid: String? = "staff-picks",
121+
categoryId: Int? = null,
122+
regions: List<String> = listOf("US", "UK"),
123+
sponsored: Boolean = false,
124+
curated: Boolean = false,
125+
sponsoredPodcasts: List<SponsoredPodcast> = emptyList(),
126+
mostPopularCategoriesId: List<Int>? = null,
127+
): DiscoverRow {
128+
return DiscoverRow(
129+
id = id,
130+
type = type,
131+
displayStyle = displayStyle,
132+
expandedStyle = expandedStyle,
133+
expandedTopItemLabel = expandedTopItemLabel,
134+
title = title,
135+
source = source,
136+
listUuid = listUuid,
137+
categoryId = categoryId,
138+
regions = regions,
139+
sponsored = sponsored,
140+
curated = curated,
141+
sponsoredPodcasts = sponsoredPodcasts,
142+
mostPopularCategoriesId = mostPopularCategoriesId,
143+
)
144+
}
145+
}

modules/services/deeplink/src/main/kotlin/au/com/shiftyjelly/pocketcasts/deeplink/DeepLink.kt

+5
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ data object ImportDeepLink : IntentableDeepLink {
245245
.setData(Uri.parse("pktc://settings/import"))
246246
}
247247

248+
data object StaffPicksDeepLink : IntentableDeepLink {
249+
override fun toIntent(context: Context) = Intent(ACTION_VIEW)
250+
.setData(Uri.parse("pktc://discover/staffpicks"))
251+
}
252+
248253
data class PlayFromSearchDeepLink(
249254
val query: String,
250255
) : DeepLink

modules/services/deeplink/src/main/kotlin/au/com/shiftyjelly/pocketcasts/deeplink/DeepLinkFactory.kt

+18
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class DeepLinkFactory(
6363
WebPlayerShareLinkAdapter(webPlayerHost),
6464
OpmlAdapter(listOf(listHost, shareHost)),
6565
ImportAdapter(),
66+
StaffPicksAdapter(),
6667
PodcastUrlSchemeAdapter(listOf(listHost, shareHost, webBaseHost)),
6768
PlayFromSearchAdapter(),
6869
AssistantAdapter(),
@@ -434,6 +435,7 @@ private class ShareLinkNativeAdapter : DeepLinkAdapter {
434435
"upgrade",
435436
"redeem",
436437
"settings",
438+
"discover",
437439
)
438440
}
439441
}
@@ -556,6 +558,7 @@ private class OpmlAdapter(
556558
"settings",
557559
"subscribeonandroid.com",
558560
"www.subscribeonandroid.com",
561+
"discover",
559562
)
560563
}
561564
}
@@ -575,6 +578,21 @@ private class ImportAdapter : DeepLinkAdapter {
575578
}
576579
}
577580

581+
private class StaffPicksAdapter : DeepLinkAdapter {
582+
override fun create(intent: Intent): DeepLink? {
583+
val uriData = intent.data ?: return null
584+
val scheme = uriData.scheme
585+
val host = uriData.host
586+
val path = uriData.path
587+
588+
return if (intent.action == ACTION_VIEW && scheme == "pktc" && host == "discover" && path == "/staffpicks") {
589+
StaffPicksDeepLink
590+
} else {
591+
null
592+
}
593+
}
594+
}
595+
578596
private class PodcastUrlSchemeAdapter(
579597
excludedHosts: List<String>,
580598
) : DeepLinkAdapter {

0 commit comments

Comments
 (0)