Sample app deeplinking

This commit is contained in:
Dimitry Ivanov 2020-07-03 16:16:25 +03:00
parent 9dce1c8533
commit 78ec885294
6 changed files with 166 additions and 24 deletions

View File

@ -18,6 +18,12 @@ android {
resConfig 'en' resConfig 'en'
setProperty("archivesBaseName", "markwon-$versionName") setProperty("archivesBaseName", "markwon-$versionName")
final def scheme = 'markwon'
buildConfigField 'String', 'DEEPLINK_SCHEME', "\"$scheme\""
manifestPlaceholders += [
'deeplink_scheme': scheme
]
} }
dexOptions { dexOptions {

View File

@ -16,10 +16,20 @@
tools:ignore="AllowBackup,GoogleAppIndexingWarning"> tools:ignore="AllowBackup,GoogleAppIndexingWarning">
<activity android:name=".sample.MainActivity"> <activity android:name=".sample.MainActivity">
<!-- launcher intent -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<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:scheme="${deeplink_scheme}" />
</intent-filter>
</activity> </activity>
<activity <activity
@ -39,8 +49,8 @@
<data android:pathPattern=".*\\.md" /> <data android:pathPattern=".*\\.md" />
<data android:pathPattern=".*\\..*\\.md" /> <data android:pathPattern=".*\\..*\\.md" />
<data android:pathPattern=".*\\..*\\..*\\.md"/> <data android:pathPattern=".*\\..*\\..*\\.md" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.md"/> <data android:pathPattern=".*\\..*\\..*\\..*\\.md" />
</intent-filter> </intent-filter>
</activity> </activity>

View File

@ -0,0 +1,73 @@
package io.noties.markwon.app.sample
import android.net.Uri
import io.noties.debug.Debug
import io.noties.markwon.app.BuildConfig
import io.noties.markwon.sample.annotations.MarkwonArtifact
sealed class Deeplink {
data class Sample(val id: String) : Deeplink()
data class Search(val search: SampleSearch) : Deeplink()
companion object {
fun parse(data: Uri?): Deeplink? {
Debug.i(data)
@Suppress("NAME_SHADOWING")
val data = data ?: return null
if (BuildConfig.DEEPLINK_SCHEME != data.scheme) return null
return when (data.host) {
// markwon://sample/20202827
"sample" -> parseSample(data.lastPathSegment)
// markwon://search?a=ext-latex&q=text
"search" -> parseSearch(data.query)
else -> null
}
}
private fun parseSample(id: String?): Sample? {
if (id == null) return null
return Sample(id)
}
private fun parseSearch(query: String?): Search? {
Debug.i("query: '$query'")
val params = query
?.split("&")
?.map {
val (k, v) = it.split("=")
Pair(k, v)
}
?.toMap()
?: return null
val artifact = params["a"]
val tag = params["t"]
val search = params["q"]
Debug.i("artifact: '$artifact', tag: '$tag', search: '$search'")
val sampleSearch: SampleSearch? = if (artifact != null) {
val encodedArtifact = MarkwonArtifact.values()
.firstOrNull { it.artifactName() == artifact }
if (encodedArtifact != null) {
SampleSearch.Artifact(search, encodedArtifact)
} else {
null
}
} else if (tag != null) {
SampleSearch.Tag(search, tag)
} else if (search != null) {
SampleSearch.All(search)
} else {
null
}
if (sampleSearch == null) {
return null
}
return Search(sampleSearch)
}
}
}

View File

@ -2,7 +2,10 @@ package io.noties.markwon.app.sample
import android.os.Bundle import android.os.Bundle
import android.view.Window import android.view.Window
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import io.noties.markwon.app.App
import io.noties.markwon.app.sample.ui.SampleFragment
import io.noties.markwon.app.sample.ui.SampleListFragment import io.noties.markwon.app.sample.ui.SampleListFragment
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity() {
@ -11,9 +14,27 @@ class MainActivity : FragmentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (supportFragmentManager.findFragmentById(Window.ID_ANDROID_CONTENT) == null) { if (supportFragmentManager.findFragmentById(Window.ID_ANDROID_CONTENT) == null) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.add(Window.ID_ANDROID_CONTENT, SampleListFragment.init()) .add(Window.ID_ANDROID_CONTENT, SampleListFragment.init())
.commitNowAllowingStateLoss() .commitNowAllowingStateLoss()
// process deeplink if we are not restored
val deeplink = Deeplink.parse(intent.data)
val deepLinkFragment: Fragment? = if (deeplink != null) {
when (deeplink) {
is Deeplink.Sample -> App.sampleManager.sample(deeplink.id)
?.let { SampleFragment.init(it) }
is Deeplink.Search -> SampleListFragment.init(deeplink.search)
}
} else null
if (deepLinkFragment != null) {
supportFragmentManager.beginTransaction()
.replace(Window.ID_ANDROID_CONTENT, deepLinkFragment)
.addToBackStack(null)
.commit()
}
} }
} }
} }

View File

@ -15,6 +15,10 @@ class SampleManager(
SampleUtils.readSamples(context) SampleUtils.readSamples(context)
} }
fun sample(id: String): Sample? {
return samples.firstOrNull { id == it.id }
}
fun samples(search: SampleSearch?, callback: (List<Sample>) -> Unit): Cancellable { fun samples(search: SampleSearch?, callback: (List<Sample>) -> Unit): Cancellable {
var action: ((List<Sample>) -> Unit)? = callback var action: ((List<Sample>) -> Unit)? = callback

View File

@ -98,11 +98,20 @@ class SampleListFragment : Fragment() {
// } // }
val state: State? = arguments?.getParcelable(STATE) val state: State? = arguments?.getParcelable(STATE)
Debug.i(state) val initialSearch = arguments?.getString(ARG_SEARCH)
// clear it anyway
arguments?.remove(ARG_SEARCH)
Debug.i(state, initialSearch)
pendingRecyclerScrollPosition = state?.recyclerScrollPosition pendingRecyclerScrollPosition = state?.recyclerScrollPosition
if (state?.search != null) {
searchBar.search(state.search) val search = listOf(state?.search, initialSearch)
.firstOrNull { it != null }
if (search != null) {
searchBar.search(search)
} else { } else {
fetch() fetch()
} }
@ -144,35 +153,38 @@ class SampleListFragment : Fragment() {
val appBarTitle: TextView = appBar.findViewById(R.id.app_bar_title) val appBarTitle: TextView = appBar.findViewById(R.id.app_bar_title)
val appBarIconReadme: ImageView = appBar.findViewById(R.id.app_bar_icon_readme) val appBarIconReadme: ImageView = appBar.findViewById(R.id.app_bar_icon_readme)
val isInitialScreen = type is Type.All val isInitialScreen = fragmentManager?.backStackEntryCount == 0
appBarIcon.hidden = isInitialScreen appBarIcon.hidden = isInitialScreen
appBarIconReadme.hidden = !isInitialScreen appBarIconReadme.hidden = !isInitialScreen
val type = this.type val type = this.type
if (type is Type.All) {
val (text, background) = when (type) {
is Type.Artifact -> Pair(type.artifact.displayName, R.drawable.bg_artifact)
is Type.Tag -> Pair(type.tag.tagDisplayName, R.drawable.bg_tag)
is Type.All -> Pair(resources.getString(R.string.app_name), 0)
}
appBarTitle.text = text
if (background != 0) {
appBarTitle.setBackgroundResource(background)
}
if (isInitialScreen) {
appBarIconReadme.setOnClickListener { appBarIconReadme.setOnClickListener {
context?.let { context?.let {
val intent = ReadMeActivity.makeIntent(it) val intent = ReadMeActivity.makeIntent(it)
it.startActivity(intent) it.startActivity(intent)
} }
} }
return } else {
appBarIcon.setImageResource(R.drawable.ic_arrow_back_white_24dp)
appBarIcon.setOnClickListener {
requireActivity().onBackPressed()
}
} }
appBarIcon.setImageResource(R.drawable.ic_arrow_back_white_24dp)
appBarIcon.setOnClickListener {
requireActivity().onBackPressed()
}
val (text, background) = when (type) {
is Type.Artifact -> Pair(type.artifact.displayName, R.drawable.bg_artifact)
is Type.Tag -> Pair(type.tag.tagDisplayName, R.drawable.bg_tag)
else -> error("Unexpected type: $type")
}
appBarTitle.text = text
appBarTitle.setBackgroundResource(background)
} }
private fun bindSamples(samples: List<Sample>) { private fun bindSamples(samples: List<Sample>) {
@ -192,7 +204,6 @@ class SampleListFragment : Fragment() {
val scrollPosition = pendingRecyclerScrollPosition val scrollPosition = pendingRecyclerScrollPosition
Debug.i(scrollPosition) Debug.i(scrollPosition)
Debug.trace()
if (scrollPosition != null) { if (scrollPosition != null) {
pendingRecyclerScrollPosition = null pendingRecyclerScrollPosition = null
@ -242,7 +253,6 @@ class SampleListFragment : Fragment() {
} }
Debug.i(sampleSearch) Debug.i(sampleSearch)
Debug.trace()
// clear current // clear current
cancellable?.let { cancellable?.let {
@ -259,6 +269,7 @@ class SampleListFragment : Fragment() {
companion object { companion object {
private const val ARG_ARTIFACT = "arg.Artifact" private const val ARG_ARTIFACT = "arg.Artifact"
private const val ARG_TAG = "arg.Tag" private const val ARG_TAG = "arg.Tag"
private const val ARG_SEARCH = "arg.Search"
private const val STATE = "key.State" private const val STATE = "key.State"
fun init(): SampleListFragment { fun init(): SampleListFragment {
@ -283,6 +294,23 @@ class SampleListFragment : Fragment() {
return fragment return fragment
} }
fun init(search: SampleSearch): SampleListFragment {
val fragment = SampleListFragment()
fragment.arguments = Bundle().apply {
when (search) {
is SampleSearch.Artifact -> putString(ARG_ARTIFACT, search.artifact.name)
is SampleSearch.Tag -> putString(ARG_TAG, search.tag)
}
val query = search.text
if (query != null) {
putString(ARG_SEARCH, query)
}
}
return fragment
}
fun markwon(context: Context): Markwon { fun markwon(context: Context): Markwon {
return Markwon.builder(context) return Markwon.builder(context)
.usePlugin(MovementMethodPlugin.none()) .usePlugin(MovementMethodPlugin.none())