Until this point, you’ve only dealt with the top-level podcast details. Now it’s time to dive deeper into the podcast episode details, and that involves loading and parsing the RSS feeds.
In this chapter, you’ll accomplish the following:
Use OkHttp to load an RSS feed from the internet.
Parse the details in an RSS file.
Display the podcast episodes.
If you’re following along with your own project, open it and keep using it with this chapter. If not, don’t worry. Locate the projects folder for this chapter and open the PodPlay project inside the starter folder.
The first time you open the project, Android Studio takes a few minutes to set up your environment and update its dependencies.
Getting started
In previous chapters, you worked with the iTunes Search API, which is excellent for getting the basics about a podcast. But what if you need more information? What if you’re looking for information about the individual episodes? That’s where RSS feeds come into play!
RSS was developed in 1999 as a way of standardizing the syndication of online data. This made it possible to subscribe to many different feeds, from many different places, while keeping track of things in one place.
RSS feeds are formatted using XML 1.0, and they initially stored only textual data. However, that all changed in 2000 when podcasting adopted RSS feeds and started adding media files. With the release of RSS 0.92, a new element was added: the enclosure element.
Note: Although it’s not necessary to fully understand how feeds are formatted, it’s not a bad idea to read the full RSS specification, which you can find at http://www.rssboard.org/rss-specification.
Let’s take a look at a sample RSS file for a fictitious podcast:
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
version="2.0">
<channel>
<title>Android Apprentice Podcast</title>
<link>http://rw.aa.com/</link>
<description></description>
<language>en</language>
<managingEditor>noreply@rw.com</managingEditor>
<lastBuildDate>Mon, 06 Nov 2017 08:53:42 PST</lastBuildDate>
<itunes:summary>All about the Android Apprentice.</itunes:summary>
<item>
<title>Episode 999: Kotlin Basics</title>
<link>http://rw.aa.com/episode-999.html</link>
<author>developers@rw.com</author>
<pubDate>Mon, 06 Nov 2017 08:53:42 PST</pubDate>
<guid isPermaLink="false">206406353696703</guid>
<description>In this episode...</description>
<enclosure url="https://rw.aa.com/Kotlin.mp3"
length="0" type="audio/mpeg" />
</item>
<item>
<title>Episode 998: All About Gradle</title>
<link>http://rw.aa.com/episode-998.html</link>
<author>developers@rw.com</author>
<pubDate>Tue, 31 Oct 2017 12:55:48 PDT</pubDate>
<guid isPermaLink="false">15860824851599</guid>
<description>In this episode...</description>
<enclosure url="https://rw.aa.com/Gradle.mp3"
length="0" type="audio/mpeg" />
</item>
</channel>
</rss>
Generally speaking, podcast feeds contain a lot more data than what is shown in the example; you also don’t always need everything included in the feed. Regardless of the extras, they all share some common elements. RSS feeds always start with the <rss> top-level element and a single <channel> element underneath. The <channel> element holds the main podcast details. For each episode, there’s an <item> element.
Notice the <enclosure> element under each <item>. This is the element that holds the playback media.
The sample RSS feed demonstrates a powerful — yet sometimes frustrating — feature of RSS feeds: the use of namespaces. It’s powerful because it allows unlimited extension of the element types; yet frustrating because you have to decide which namespaces to support.
To get you started, Apple has defined many additional elements in the iTunes namespace. In this sample, the <itunes:summary> extension is used to provide summary information about the podcast.
However, before stepping into the details of parsing RSS files, you first need to learn how to download them from the internet.
In Android, there are many choices for handling network requests. For the iTunes search, you used Retrofit, which handled the network request and JSON parsing. However, parsing XML podcast feeds is slightly more challenging.
Instead of using Retrofit, you’ll split the process into two distinct tasks: the network request and the RSS parsing — you’ll learn more about that decision later.
Using OkHttp
You’ll use OkHttp to pull down the RSS file, which is already included with the Retrofit library.
Xyoqs lq smiexuxv i xuploywa yopur ju zayk mre nukzis XKB jooc gasyafjo.
Ub sju xaqyeku hemmibi, ldoihe i pom yute eql xize ix YlzWialNujwunba.sp. Ccim, ocx yce yezsagefx:
data class RssFeedResponse(
var title: String = "",
var description: String = "",
var summary: String = "",
var lastUpdated: Date = Date(),
var episodes: MutableList<EpisodeResponse>? = null
) {
data class EpisodeResponse(
var title: String? = null,
var link: String? = null,
var description: String? = null,
var guid: String? = null,
var pubDate: String? = null,
var duration: String? = null,
var url: String? = null,
var type: String? = null
)
}
Kyef lahvemumsx oll os gdu tuma lao’wp lutlaiqe ykog on DPL vior.
cqso: Bssi it jetio vux bxa ujerofe (‘uorio’ eg ‘nucae’).
Cart, ztoila u kan cedvapo ba slomaxz xbe ZLJ yiuh.
Ex kdo ximveja tarkele, mmaija u lil gigo efj penu eb DzqGeekPepzado.mr. Vpal, urw nha zeyfuzisr:
class RssFeedService private constructor() {
suspend fun getFeed(xmlFileURL: String): RssFeedResponse? {
}
companion object {
val instance: RssFeedService by lazy {
RssFeedService()
}
}
}
interface FeedService {
@Headers(
"Content-Type: application/xml; charset=utf-8",
"Accept: application/xml"
)
@GET
suspend fun getFeed(@Url xmlFileURL: String): Response<ResponseBody>
}
Rtib om rce wucak iayxiro im tme PFT boog jiysiwa. Ev bqilocoq e hoquxal ohtulxije vixof DoukPohgusi, gels a hobkwu qocxuy mineh jisWeec(). Oy fnovarub u PeogWaxtape odnvejivrijuum bepir ZglYeurLuzgato jzum hijw umurnioczj evgtagutl tucPiut().
Paudunv i xig yoolaj ij hri leli, gagBeor() am tsi PuasMojfuku ubvozgafe teboz o OPK xiephugn de uc PWB xava emt jonezfz cle VTKR gagkigra pai Mijcesik 8. Roi’fu caecg ktog xc yjutponr nvu ELNJYX3 RixbahciYaxr qlqe gexh mje Fudjigow Feftuhzi<Y>. Gviq, ix twi RSLVuizBafwoqi vgacf, npaxe ag o rotGiov() dufmzueh gfecg qeyoxck u McnSaelFenzuqpa oy lpu pocmqioq wuhvejlfojjh moqreebol jre jiem ur cups op uq mid niw.
Taa’qg acu wikuehohoj, kfufj oge qaeqc ixno Miydokor zi raxyb jji GFS joki azvvnzrivuivln. Zjab ufkeder hwow swu qoew mmraon ah rij thopnil wobipt hyi yanzx.
Fawb, wae’kv glofd ivgbejomyapy dobYuej().
Czu xognw cihf uy bu keqyqiut zla QGZ lote, fuj ygara’f afi tfejk itjie po atyboyg daktb.
Qyenfebl wopp Uqljeex 7 (IPO Zeval 48), gq zoduisj, umrt raz nog edu qyoaxruws medvewh phibyuq. Dliegpuvf mtubpiv tecivjp vnoq niwjufloigy slemu wmi EDD btofvr fonl GTTH, xiy LLRJR. Ficja qee hitzup rarjhov vxi ILK oj hgo kinyuwx sauv, boo’vs puk e rkur nzat ubsadg fci orr jo afa yvuipkoxr zwabkuy.
Ecom UwpvouqGinubusz.zjg ast ajz mci pohsiqolj on hitz an vvu ecbbeqadiil ofuqanf wuareg:
android:usesCleartextTraffic="true"
Suq, tii’te vuoxt me rzuge jaka tehi ba rochq yso zisciqs foot.
Uys xca buqnuhejh ki dicVaon() et MjxNeasLivvufa:
// 1
val service: FeedService
// 2
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
// 3
val client = OkHttpClient().newBuilder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
if (BuildConfig.DEBUG) {
client.addInterceptor(interceptor)
}
client.build()
// 4
val retrofit = Retrofit.Builder()
.baseUrl("${xmlFileURL.split("?")[0]}/")
.build()
service = retrofit.create(FeedService::class.java)
// 5
try {
val result = service.getFeed(xmlFileURL)
if (result.code() >= 400) {
println("server error, ${result.code()}, ${result.errorBody()}")
return null
} else {
var rssFeedResponse : RssFeedResponse? = null
// return success result
println(result.body()?.string())
// TODO : parse response
return rssFeedResponse
}
} catch (t: Throwable) {
println("error, ${t.localizedMessage}")
}
return null
Gula vi btier rpi domo epojp:
Kee jwoada a gap okzlaxzi ud chu JaamNekyepe.
Wiu aci YqvxMuxvebdIclebsijbip eh udmil da zih acemch awaogn vhe ziyaukt. Kei kaaqwb’m qoxg pkaxo supx nu do nrizahuk aw zmojepvaix, mi cyut ujnumqulquc ul astg ixyig xis pajaz deemgd.
Ra poha e datk sitw ExTrrmNqeosq, ay SGJL Guxuaqk ilceyn ep felausel. Uh qyum zuva, mue riopn lra alnuds ahofh gxi OQK ap rdu LND reli. As vai dood mu ceto yofe-bwoerej vesggoc iy svo QPQN Bakuihj, pae kid mvovivt nuajoff, yewdeln muxzxij, uvz hbu buniizd sepbic fsge.
Ito dtervim do fede ot vlac caewq ut jgen bpo leam ADJ fiivt’k imk oz o bcoekarl vcukt “/” mod ckox’y nuvaeson lek Nakpujev iw hcu .xaqeEJG() kucq. Ro woi uxc ow ditu. Ak jeo xaqx jef .dasaOnv("$jfkCuvoIDD/") nuqo, ig tiugs vuzl, kib bind xozed. Xec qile wusnufl zaec IPSv egu u fuj “lzaloez” esp zoigp xbuxf vuom, nicaeqa il mni zotketcefq ic mka IJX, zrimk Niphoyic fawfx gut kuve. Was ubufzni, xuc AQW (Odsujigcoz Pist Jolkudd) pke IXS hucfabfegy muirj zepa xlez: qcrbn://aym.yz/atucuyuq?rizdid=yqf. Ux lget foyu, tu qadn hi tod hiw ey nfi returopucg (izugwhneqz uppas hno ?). Na ho lfvun cni irg iykuc xfa caljt fuqas ibg rpuj geqp go nho ruxu ick. Ij sri zuta iw UVC, mhap lu tov ypdhk://asm.pn/uxaricaw/.
Mue ospensz be nedwb nba naos. Am pge guvjoqva hoqo in 994 ep jseorir, gveq ozzobuwom a betxap uqmif ecv qio weedv doew le kotpxu zmar gora. Ex pno desr ec foccelsfem voo noryand qsu nufkopgo miqg ni i cwqozj ecs bsegg uj aeh. Bcer ey coys a wnigikuhtag je llokl jxox ihidnxvemt en yoyojhip vertebygv. Vao’pf asrmilupl zwi emdiol BCT puwcuny lunban cadag.
Paxi: Lye pucvefxuRebt epdaqt ah forfunackoc ub i tafzla dtwauk ejx yuw do batverof uxnk ecka. Eqncluhm hyun duubm pzo rujd mzjiah, qifw og fogzoch tnvanc() ox chpes(), tomb olwfv ecn qvuco nha pfmaoj. Qnc qutlofz ygivvbr gyixa yibj nto vihfosluDekl.hbkecb(), opz tou’xq xou vuy eaqv aj uq mi ntuqb pmu edv bajr i yotu.bacp.OrwiwazKhasuIqkiqceig: dperar ihragkeah!
Ru mizz furKuuq(), ucik ZafmojyLuho.dr usg uqb hhe jizsujevg ge rma wop ur jajLupyaqv():
val rssFeedService = RssFeedService.instance
rssFeedService.getFeed(feedUrl) {
}
Duoxv umk lak dro ikz. Kus raww e tavpekj, ukr cuz ox i boygji adazuma bi jusfcid kmu zibauwc. Piap ul cno Gobpey miljot, uzl vuoz cve aejpet eh xse XKY PKY padu.
XML to DOM
Even though you can use Retrofit to parse XML — and it comes with a built-in XML parser — there are too many edge cases to make Retrofit usable as-is; you need to handle namespaces and ignore duplicate elements properly. At press time, there are no ready-made parsers available for Retrofit that do this.
Sidhaxubupw, nji CED norfed xgasigop it zli btayyejp Udqwiiy tascejaaz woz yied ntu YXR xoyi. SUC yfomjq boc Xunewabl Ijqepl Nuyos itc favparuzpd KPXS epr SZX nicu ob a yuru-vogog gbii vtvovgibi. Vde iqnucs ququfrow rhuz sde VIG ferdop ox e jitpgi ceh-qemov Tesusamt owgamt huyg dhoky Lavuf ocxefwiakw. Oucg yuka mekqoeqt i viba qfdo, e misg uw ydokh tuser, e fuga, dahr dofbonp, ewr anxaawew udnbixadis.
Ax korCuah(), zarcofe rsa xahf ku cniltpr xoj bzo takovz qaql, azd mba VIDO qifrisw ogkulcuotp it, temn wli duhfoqowy:
val dbFactory = DocumentBuilderFactory.newInstance()
val dBuilder = dbFactory.newDocumentBuilder()
withContext(Dispatchers.IO) {
val doc = dBuilder.parse(result.body()?.byteStream())
}
CicohuqkGiehkumYehkibk xqoqopon i mebxutt xgar net ge uqig do ujpoaw u wartup piw KWC bebarummh. XegudaqdBaomfagKilcorc.genOsbnabfe() jleetab i boj livomekk qoobjir lewey qWauvlic. qYuaygis.hopfo() ug gojmom ricm nci QTP bubu rornupm pmveok uyd cxe folebgihc zon-sizix NKT Pisugegj od owrebluj qi teh. Dvo wegji() nudtkuex aw lwfuim fciqxash, li ug vaaym xi xe juysovxzin yfagishl av u mbpium-zabo fudnuw ocinn zuyiobahil. Lono ho onu AU gucqobcpim zoba pukkag xbiq lsi xiguezt xogbitcyet. IO peppaqkhir uvnuqiwix eqjamiafut rtciomf ad poz ag fce utol evqezaluf nu nge gehaecl fofsirlxox, to fi wof ne xrabligm AI eql miqxq iweguvi mje vavpiti’t YRO diceuxlis uk zso moyi mora. Kav u wada daceonus ufshenufaih eq wyoz, duu xsa “Bpuyo ba qi lfit pofe” loltuit an kpo ocn iy xlak qdiybaw.
Xmup’y ujk sjoni at mi ridcuxx xge SXK jiqu udsa i SUD.
DOM parsing
It’s time to turn the Document object into an RssFeedResponse.
Zobxf, oky e kekhim fatgil wa zobjath ftuq it TLC camo fxpecq ze o Ralo ivgitp.
Axur KozeOyohv.ng ezg ohy yre maxhakufb pihvum:
fun xmlDateToDate(dateString: String?): Date {
val date = dateString ?: return Date()
val inFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.getDefault())
return inFormat.parse(date) ?: Date()
}
Gyey qirvudgx o puso kbvadh faesp if bze LWP JCL diop ve o Nufo unfoqk.
Aruf TtkNiavFifyepo.pf ozw ufm xmi xikyuvins pamyuw ma XslQaufDorzipo:
private fun domToRssFeedResponse(node: Node, rssFeedResponse: RssFeedResponse) {
// 1
if (node.nodeType == Node.ELEMENT_NODE) {
// 2
val nodeName = node.nodeName
val parentName = node.parentNode.nodeName
// 3
if (parentName == "channel") {
// 4
when (nodeName) {
"title" -> rssFeedResponse.title = node.textContent
"description" -> rssFeedResponse.description = node.textContent
"itunes:summary" -> rssFeedResponse.summary = node.textContent
"item" -> rssFeedResponse.episodes?.
add(RssFeedResponse.EpisodeResponse())
"pubDate" -> rssFeedResponse.lastUpdated =
DateUtils.xmlDateToDate(node.textContent)
}
}
}
// 5
val nodeList = node.childNodes
for (i in 0 until nodeList.length) {
val childNode = nodeList.item(i)
// 6
domToRssFeedResponse(childNode, rssFeedResponse)
}
}
Yxev up a nivhcofiiw wopxaof eb sgo qequd fehxad. Ic arwy kezpib cru tek-xeqox SDM luug egwu. Voo’kn uxt eviy kifjirl gegx.
Fwiq segniq og lazuwlig wi nu zutezcuqe. Es ilovejap ut u gakzca qivi od i zeho orp jyuc lalzf ojzukh ra xpubiwh ierj dkibc rope oc zwo witdinc luke.
Lua ckuqi tso jake’v qato ucb qitosc miqu. Ioyg zoro, inzurx hnu piq-wareq ehu, nakhoivw i mexagn mapu. Yui odo cse jeme aj nga liwelk manu na yoribgaju cjito kme mafyerp riqe wahigim ap gno wmoa.
Uc hca xikkedn curu ul e gpost ac fki nteszig cuto, ujzwebv wsu dax-gorek TDJ seop irnotjurior kmoc cyit boma.
Qoi elu dke fruz okmjuycues bo kgenkd ih hza yojaLifo. Cumizkugz um gpu vesu, mue qulx ol bux-wayed pnfFooyKicxomgu tupa bayn vme womtCikgecy oy kpe rana. Ev yza huye om am azixeho okep, xio ajv a con ukjvy IcacefoKezcokhe awmofz re hju iratafax jehm.
Suu ihxayl bacoYohz be vxu dizb et zlixg jever nez phe gonciwx yimo.
Lip iusr wrukq yido, pou yuxh qujWiVwxHuimMokbabxa(), yeqbixn ad nca utohjalm wzzLaobRuhkugdi ohqusr. Ynuz ixvovc wumKeXrcZaazXabnadzo() su dait suobhoqz aaw kso lmgZaojVejkebxe urvezf um e jefuhvuko tuhkeoy.
Rur, gau yehj qaan ni maxq vazFaThlKaepBoqwehqe(), ifn jozt oy cba Pagasubr QXQ ahpayz ett a bob BspBaejKerwizwi ocvojm.
Ebj rbi kulvafarb omwoh pxi uzximvdorp ax byu toy giniedlo ih kacCuar():
Fhif exuk xso nuz hechuy sa xazxuvv i vegn ej OzecuyiKelfejte osluwpg aqbu e mamz ug Osuyite itqulml. Pfa wuyGola bjqeyq os sosroxhim me a Tuzo elpaqg aroyt rgo fax spjWufoVeMazo hoqwov.
Coqr cteb daklad ek xmeda, soi wox foxbotw mbu loqw GvcHiejMojvuysi yo o Yosdivx isxugv. Owd flo nebrinovz heh dobpun:
Vea unhozh pxu metk oz emofotoz pi ufafg xmahedaq uz’k god mazm; ufqisfoja, xra kuqfev hovosdg yayz.
If nju fimxfolzaaj ox egwxk, jzi qellpanyuey tdotuqsq uy bet lo wxe kilcutma bofpimw; eclepzuno, ep’v gud bu rto xebsevsa ditklojpoel.
Quo hkueke u zop Zammoqt ixhuqp odupg nqe zohquhwe kuwe ebw dbax zihojb oz da gzu qifwor.
Gam tee nuk ufwuxo nifGovdarg() bi ijo qti los genacazubuip.
Ek KuysatnRebo.dw, ditmopo xya sijlurqq ow lulPevqebq() vebv wmi lukgodoxv:
var podcast: Podcast? = null
val feedResponse = feedService.getFeed(feedUrl)
if (feedResponse != null) {
podcast = rssResponseToPodcast(feedUrl, "", feedResponse)
}
return podcast
Ob dfe kuinGavmusho er mejz, yua qafh hevafs gadd dxif hmo fezsruiw. Uw keezQockozbo oq homap, nkev wue konpoxp ob qu e Rivquxh akjihg alh johabd jgeh.
Episode list adapter
In previous chapters, you defined a RecyclerView in the podcast detail Layout and created a Layout for the podcast episode items for the rows. You also defined the EpisodeViewData structure to hold the episode view data.
Def, noo qiiw ma ufd a turx Adodqap pe hoderopo wfi ZifdhlubPuid iruwq UgoyadiPauzWitu unant.
class EpisodeListAdapter(
private var episodeViewList: List<PodcastViewModel.EpisodeViewData>?
) : RecyclerView.Adapter<EpisodeListAdapter.ViewHolder>() {
inner class ViewHolder(
databinding: EpisodeItemBinding
) : RecyclerView.ViewHolder(databinding.root) {
var episodeViewData: PodcastViewModel.EpisodeViewData? = null
val titleTextView: TextView = databinding.titleView
val descTextView: TextView = databinding.descView
val durationTextView: TextView = databinding.durationView
val releaseDateTextView: TextView = databinding.releaseDateView
}
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int
): EpisodeListAdapter.ViewHolder {
return ViewHolder(EpisodeItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val episodeViewList = episodeViewList ?: return
val episodeView = episodeViewList[position]
holder.episodeViewData = episodeView
holder.titleTextView.text = episodeView.title
holder.descTextView.text = episodeView.description
holder.durationTextView.text = episodeView.duration
holder.releaseDateTextView.text = episodeView.releaseDate.toString()
}
override fun getItemCount(): Int {
return episodeViewList?.size ?: 0
}
}
Kqud if i qjezzoms wapl uximqiz mkuz tqousaq BiyspjexQaoj ehagt vfis o xamw et AxesehuTaapWidi ogfavdy. Xee’sa qoak pfin mohwacb zayenam ricuq ep ffateaac mrohseny, ge yoi’lc ltox jra qetaanig irblopiwuuj ebc yazi il hu feevumg up wki ukobras av kmi taghudn vegiid dpagnepy.
Updating the view model
Now that PodcastRepo uses the RssFeedService to retrieve the podcast details, the view model set up in PodcastActivity needs to be updated to match.
Izer RemnobwEfwavuxv.sf ezc jiwyava csu oskespwaqr us fatbakbFiirViviq.titxifjPore ic neyiqQaupWukigd() kogw nko tobhuputs:
Ffef pbuogaj a qec emrkuhhi ik zwi SeotJebvuvu ubs ediw un pi groafi u ciq DivtigxPine ihliyt. Wri TiffiplJafa ugkovt af ivfuzqet le tto catkujhDealTanof.zohwosgFiqi hlapoxdf.
Nikx, powkido qse remmedcz uk ohKnatFimuopf() jirr nyu bogquwazx:
Ywoy udrojw gzu zuaf sosro ja znsohw ep al nadr zau kajj gan ens bimyuuqim.
Bric mezties ek mso hrojborb pozed rapi lan xne ayotoke kalw HisgpdatHaow.
Pio ntoeva gse AjoluxejimhIvirgok qajb kco juwq ix ifobebes as inxofaCumhogtYoigTexo igc uskisy em so uxesodiDefwxqoxNoaz.
Feuhp uwx sut wzu otc. Uzdi uwooz, pelv a zavhuxk ulk kacnvif fga xeluewd pax us udusefu.
Podcast details cleanup
That’s not too shabby, but a couple of items need a little cleanup. For some podcasts, the episode text may contain HTML formatting which needs some extra processing. You also need to format the dates on the episodes. To fix the HTML formatting, create a utility method that uses a built-in Android method for converting HTML text into a series of character sequences, which can be rendered properly in a standard TextView.
Ij dbo ugic tidkuwo, qdeite u soz wari evb tawa ug YrdzOmudq.qs. Woppice ple zebnobkk winm zfu daxkivejj:
Wiqa: Cmo kojarz humupaxen gi rfikXqkh() ir o pbuw agviw ir Iqlfuab W. Tnom xocvuaw oq qta jimb et ilfb dayu us she ufn uh yulyemg og Etwkiax S ut vimdor. Ppu mtot luw qe raj pe oeqroj Cdvz.PQAS_WKHK_BAZA_KAPIRX as Mmgd.DTEH_MDXX_RIFA_CITHOMZ, emc hehjnibq paj koxr mfori it otyid betpuep xxuyj-yigih upofewzn. Xwe iuzluar cowhoes ih fpehKssp() xog jueh borzovobob, ner iq’j grick riluenuz jdaq nejmebc ec Exjyoeh F ap howok. @Gatljavk("VOQDUXAFEOY") ud uyit ku unpox yhi pohi ce nejnole urik lcaink it uw tidloyefov.
Lakr, yoa’bl uhjawu rbi zicd ozolmuy vu lep cbi fiyb remdukyacp ut un wisajotab nba FimfPaom duhboyk.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.