From 88dc8a3dc8902988bc582078ef69e2b0ae88b990 Mon Sep 17 00:00:00 2001 From: lunaticbum <> Date: Thu, 29 Aug 2024 18:35:10 +0900 Subject: [PATCH] ... --- .../rasel/lunar/launcher/home/LauncherHome.kt | 330 +++++++++++------- .../todos/LinearLayoutManagerWrapper.kt | 15 + .../rasel/lunar/launcher/todos/RssDataItem.kt | 17 + .../lunar/launcher/todos/RssFeedsParser.kt | 186 ++++++++++ .../lunar/launcher/todos/RssItemAdapter.kt | 41 ++- .../lunar/launcher/todos/SmsLogsAdapter.kt | 6 +- .../rasel/lunar/launcher/todos/YoutubeData.kt | 133 +++++-- .../rasel/lunar/launcher/utils/DataManager.kt | 83 +++++ .../rasel/lunar/launcher/utils/HashUtils.kt | 310 ++++++++++++++++ .../rasel/lunar/launcher/utils/RssList.kt | 11 + app/src/main/res/layout/launcher_home.xml | 2 +- app/src/main/res/layout/list_item_with.xml | 40 ++- 12 files changed, 982 insertions(+), 192 deletions(-) create mode 100644 app/src/main/kotlin/rasel/lunar/launcher/todos/LinearLayoutManagerWrapper.kt create mode 100644 app/src/main/kotlin/rasel/lunar/launcher/todos/RssDataItem.kt create mode 100644 app/src/main/kotlin/rasel/lunar/launcher/todos/RssFeedsParser.kt create mode 100644 app/src/main/kotlin/rasel/lunar/launcher/utils/HashUtils.kt diff --git a/app/src/main/kotlin/rasel/lunar/launcher/home/LauncherHome.kt b/app/src/main/kotlin/rasel/lunar/launcher/home/LauncherHome.kt index ecb5ec6..6ae75dc 100644 --- a/app/src/main/kotlin/rasel/lunar/launcher/home/LauncherHome.kt +++ b/app/src/main/kotlin/rasel/lunar/launcher/home/LauncherHome.kt @@ -29,6 +29,8 @@ import android.graphics.Bitmap import android.graphics.Color import android.net.http.SslError import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.provider.AlarmClock import android.telephony.TelephonyManager import android.text.format.DateFormat @@ -74,13 +76,18 @@ import rasel.lunar.launcher.helpers.UniUtils.Companion.expandNotificationPanel import rasel.lunar.launcher.helpers.UniUtils.Companion.lockMethod import rasel.lunar.launcher.home.LauncherHome.Companion.refreshCalls import rasel.lunar.launcher.home.LauncherHome.Companion.refreshSms -import rasel.lunar.launcher.home.weather.JsonParser import rasel.lunar.launcher.home.weather.WeatherExecutor import rasel.lunar.launcher.qaccess.QuickAccess import rasel.lunar.launcher.settings.SettingsActivity +import rasel.lunar.launcher.todos.LinearLayoutManagerWrapper import rasel.lunar.launcher.todos.MissedCallsAdapter import rasel.lunar.launcher.todos.Root +import rasel.lunar.launcher.todos.RssDataItem +import rasel.lunar.launcher.todos.RssDataType +import rasel.lunar.launcher.todos.RssFeedsParser +import rasel.lunar.launcher.todos.RssItemAdapter import rasel.lunar.launcher.todos.SmsLogsAdapter +import rasel.lunar.launcher.todos.TwoColumnBrowseResultsRenderer import rasel.lunar.launcher.utils.BLog import rasel.lunar.launcher.utils.MissedCallGetter import rasel.lunar.launcher.utils.RecentSmsGetter @@ -90,7 +97,6 @@ import rasel.lunar.launcher.utils.SimpleFingerGestures import java.io.ByteArrayInputStream import java.io.InputStream import java.nio.charset.Charset -import java.nio.charset.StandardCharsets import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date @@ -117,6 +123,10 @@ internal class LauncherHome : Fragment() { var smsList = arrayListOf() var listTags = arrayListOf() var listItem = arrayListOf() + var rssSet = hashMapOf() + var rssUrls = arrayListOf() + var feddsUrls = arrayListOf() + var rssList = arrayListOf() fun refreshSms() { Executors.newSingleThreadScheduledExecutor().schedule({ @@ -158,14 +168,15 @@ internal class LauncherHome : Fragment() { batteryReceiver = BatteryReceiver(binding.batteryProgress) mMissedCallsAdapter = MissedCallsAdapter(callList, requireContext()) mSmsLogsAdapter = SmsLogsAdapter(smsList, requireContext()) + mRssAdapter = RssItemAdapter(rssList, requireContext()) binding.favAppsGroup.visibility = View.GONE - + binding.mainList.layoutManager = LinearLayoutManagerWrapper(requireContext()) workmanager()?.getWorkInfosByTagLiveData(SMS_WORK_TAG)?.observeForever { binding.recentSms.text = "최근 문자 [${smsList.size}]" if (binding.recentSms.isChecked) chooseAdpater() - else if (missedCalls.size == 0) { + else if (binding.missedCalls.isChecked && missedCalls.size == 0) { binding.recentSms.isChecked = true chooseAdpater() } @@ -175,7 +186,7 @@ internal class LauncherHome : Fragment() { workmanager()?.getWorkInfosByTagLiveData(CALL_WORK_TAG)?.observeForever { binding.missedCalls.text = "최근 통화 [${missedCalls.size}]" - if (missedCalls.size > 0) binding.missedCalls.isChecked = true + if (binding.recentSms.isChecked && missedCalls.size > 0) binding.missedCalls.isChecked = true if (binding.missedCalls.isChecked) chooseAdpater() it.clear() } @@ -192,6 +203,7 @@ internal class LauncherHome : Fragment() { lateinit var mMissedCallsAdapter : MissedCallsAdapter lateinit var mSmsLogsAdapter : SmsLogsAdapter + lateinit var mRssAdapter : RssItemAdapter @@ -215,7 +227,8 @@ internal class LauncherHome : Fragment() { false } } - + binding.mainList.setHasFixedSize(true) + binding.mainList.setItemViewCacheSize(10) binding.missedCalls.setOnClickListener { binding.missedCalls.isChecked = true chooseAdpater() @@ -225,9 +238,45 @@ internal class LauncherHome : Fragment() { chooseAdpater() } binding.otherCheck.setOnClickListener { + if(rssSet.size < 10) { + rssSet.clear() + feddsUrls.clear() + feddsUrls.addAll(RssList.newsFeeds) + rssUrls.clear() + rssUrls.addAll(RssList.youtubeUrls) - doWebPare(RssList.youtubeUrls.get(0)) + binding.otherCheck.isChecked = true + rssList.clear() + rssList.add(object : RssDataItem { + override fun title(): String { + return "waiting for data" + } + + override fun thumbnailUrl(): String { + return "" + } + + override fun originPage(): String { + return "waiting for data" + } + + override fun description(): String { + return "waiting for data" + } + + override fun pubDate(): Long { + return 0L + } + + override fun category(): RssDataType { + return RssDataType.NO_DATA + } + }) + doWebPare(RssList.youtubeUrls.removeFirst()) + getFeeds(feddsUrls.removeFirst()) + } + binding.mainList.adapter = mRssAdapter } binding.otherCheck.setOnLongClickListener { @@ -244,7 +293,41 @@ internal class LauncherHome : Fragment() { // BLog.LOGE("intent >>> ${i.action}") } + fun getFeeds(url : String) { + Executors.newSingleThreadScheduledExecutor().schedule( { + RssFeedsParser.getFeeds(url).apply { + }.filter { it.pubDate() >= (System.currentTimeMillis() - 1000L * 60L * 60L * 24 * 3) + }.forEach { + BLog.LOGE("getFeeds it >> ${Gson().toJson(it)}") + rssSet.put(it.originPage(), it) + }.apply { + if (feddsUrls.size > 0) { + getFeeds(feddsUrls.removeFirst()) + } + if(rssUrls.size < 1 && feddsUrls.size < 1) { + rssList.clear() + rssSet.forEach { t, u -> + rssList.add(u) + } + rssList.sortByDescending { it.pubDate() } + Handler(Looper.getMainLooper()).post { + mRssAdapter.updateData(rssList) + } + } + } + },1,TimeUnit.SECONDS) + } + fun doWebPare(url : String) { + if (url.contains("youtube")) { + Executors.newSingleThreadScheduledExecutor().schedule( { + Jsoup.connect(url).get()?.apply { + ytChannel(this) + } + },1,TimeUnit.SECONDS) + return + } + BLog.LOGE("binding.otherCheck before ThreadRun") binding.searcher01.webViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { @@ -318,23 +401,29 @@ internal class LauncherHome : Fragment() { if (missedCalls.size > 0 && isAdded && isResumed && isVisible) { try { callList.clear() - binding.mainList.adapter = mMissedCallsAdapter + if(binding.mainList.adapter is MissedCallsAdapter){ + }else { + binding.mainList.adapter = mMissedCallsAdapter + } missedCalls.forEach { t, u -> callList.add(u) }.apply { callList.sortByDescending { it.date } - mMissedCallsAdapter.updateData(callList) + Handler(Looper.getMainLooper()).post {mMissedCallsAdapter.updateData(callList)} } } catch (e : Exception) { - } } } else if(binding.recentSms.isChecked){ if (smsList.size > 0 && isAdded && isResumed && isVisible) { try { - binding.mainList.adapter = mSmsLogsAdapter + if(binding.mainList.adapter is SmsLogsAdapter){ + + }else { + binding.mainList.adapter = mSmsLogsAdapter + } smsList.sortByDescending { it.rcvDate } - mSmsLogsAdapter.updateData(smsList) + Handler(Looper.getMainLooper()).post {mSmsLogsAdapter.updateData(smsList)} } catch (e : Exception) { } @@ -402,13 +491,13 @@ internal class LauncherHome : Fragment() { gestureDistance: Double ): Boolean { when(fingers) { - 1 -> + 2 -> if (targetView?.equals(binding.batteryProgress) ?: false) { expandNotificationPanel(requireContext()) } else { expandNotificationPanel(requireContext()) } - 2->{ + 4->{ var startIntene = Intent(Intent.ACTION_MAIN) startIntene.setComponent(ComponentName("com.samsung.android.app.interpreter","com.samsung.android.app.interpreter.interpretation.view.InterpretationActivity")) startActivity(startIntene) @@ -587,96 +676,8 @@ internal class LauncherHome : Fragment() { jGuruTag(doc) } } else if (lastedFinishedPageUrl?.contains("youtube") == true) { -// BLog.LOGE("doc youtube >>>> ${result}") val doc: Document = Jsoup.parse(result) -// BLog.LOGE("doc.html().contains(ytInitialData) ${doc.html().contains("ytInitialData", false)}\n${doc.html().toString().split("var ytInitialData")[1].split("")[0]}") - - doc.getElementsByTag("script").forEach { - if(it.html().contains("var ytInitialData", false)) {/**/ - var ytInitialData = it.html().split("var ytInitialData = '")[1].split("'")[0].toString() - ytInitialData = ytInitialData.replace("\\x22", "\"") - .replace("\\x7b", "{") - .replace("\\x7d", "}") - .replace("\\x5b", "[") - .replace("\\x5d", "]") -// BLog.LOGE("doc youtube div >>>> ${StringEscapeUtils.escapeJson(ytInitialData)}") -// BLog.LOGE("doc youtube div >>>> ${StringEscapeUtils.escapeXml10(ytInitialData)}") -// BLog.LOGE("doc youtube div >>>> ${StringEscapeUtils.escapeJson(ytInitialData)}") -// BLog.LOGE("doc youtube div >>>> ${StringEscapeUtils.escapeHtml3(ytInitialData)}") -// BLog.LOGE("doc youtube div >>>> ${StringEscapeUtils.unescapeEcmaScript(ytInitialData)}") - var tempJSONObject : JSONObject? = null - JSONObject(StringEscapeUtils.unescapeEcmaScript(ytInitialData)).apply{ - tempJSONObject = this - BLog.LOGE("tempJSONObject.toString() >>> ${tempJSONObject.toString()}") - val aRoot = Gson().fromJson(tempJSONObject.toString(), Root::class.java) - BLog.LOGE("aRoot >>> ${aRoot}") - BLog.LOGE("aRoot?.responseContext >>> ${aRoot.responseContext}") - BLog.LOGE("aRoot?.header ${aRoot?.header}") - BLog.LOGE("aRoot?.topbar ${aRoot?.topbar}") - BLog.LOGE("aRoot?.metadata ${aRoot?.metadata}") - BLog.LOGE("aRoot?.contents? >>> ${aRoot?.contents}") - BLog.LOGE("aRoot?.contents?.twoColumnBrowseResultsRenderer >>> ${aRoot?.contents?.twoColumnBrowseResultsRenderer}") - BLog.LOGE("aRoot?.contents?.twoColumnBrowseResultsRenderer.tabs >>> ${aRoot?.contents?.twoColumnBrowseResultsRenderer?.tabs}") - BLog.LOGE("aRoot?.microformat? >>> ${aRoot?.microformat}") - BLog.LOGE("aRoot?.onResponseReceivedActions? >>> ${aRoot?.onResponseReceivedActions}") - aRoot?.contents?.apply { - BLog.LOGE("aRoot?.contents? >>> ${this}") - BLog.LOGE("aRoot?.contents?.twoColumnBrowseResultsRenderer >>> ${twoColumnBrowseResultsRenderer}") - twoColumnBrowseResultsRenderer?.tabs?.forEach { - BLog.LOGE("it.sectionListRenderer?.contents? >>> ${it}") - BLog.LOGE("it.tabRenderer ${it.tabRenderer}") - BLog.LOGE("it.expandableTabRenderer ${it.expandableTabRenderer}") - } - - } - } -// .keys().forEach { -// if (it.equals("contents")) { -// var child = tempJSONObject?.getJSONObject(it) -// if(child?.length() == 1 && child?.has("singleColumnBrowseResultsRenderer") == true) { -// BLog.LOGE("(singleColumnBrowseResultsRenderer >> ${child}") -// var singleColumnBrowseResultsRenderer = child?.getJSONObject("singleColumnBrowseResultsRenderer") -// if (singleColumnBrowseResultsRenderer?.has("tabs") == true) { -// var tabs : JSONArray? = singleColumnBrowseResultsRenderer?.getJSONArray("tabs") -// BLog.LOGE("(tabs >>>> ${tabs}") -// for ( i in 0..<(tabs?.length() ?: 0)) { -// var tabsChild = tabs?.getJSONObject(i) -// BLog.LOGE("tabsChild >> ${tabsChild}") -// if(tabsChild?.has("tabRenderer") == true) { -// var tabRenderer = tabsChild?.getJSONObject("tabRenderer") -// BLog.LOGE("tabRenderer >> ${tabRenderer}") -// tabRenderer?.keys()?.forEach { -// // BLog.LOGE("tabRenderer in key >> ${it}") -// if (tabRenderer?.get(it) is String) { -// BLog.LOGE("tabRenderer String in $it >> ${tabRenderer?.get(it)}") -// } -// else if (tabRenderer?.get(it) is JSONObject) { -// var obj = tabRenderer?.getJSONObject(it) -// BLog.LOGE("tabRenderer child JSONObject in $it >> ${obj}") -// var pkey = it -// obj?.keys()?.forEach { -// jsonObjLog(pkey,it,obj) -// } -// } -// else if (tabRenderer?.get(it) is JSONArray) { -// BLog.LOGE("tabRenderer JSONArray in $it >> ${tabRenderer?.get(it)}") -// } else { -// BLog.LOGE("tabRenderer else in $it >> ${tabRenderer?.get(it)}") -// } -// } -// } -// } -// } -// } else { -// child?.keys()?.forEach { childKey -> -// BLog.LOGE("ytInitialData >>> ${it} >>>> ${childKey}") -// } -// } -// } -// } - } - } -// ytChannel(doc.html()) + ytChannel(doc) } BLog.LOGE("binding.otherCheck after ThreadRun") } @@ -691,16 +692,22 @@ internal class LauncherHome : Fragment() { var obj = jsonObject?.getJSONObject(key) BLog.LOGE("jsonObjLog $pkey JSONObject in $key >> ${obj}") obj?.keys()?.forEach { - jsonObjLog(key,it, obj) +// jsonObjLog(key,it, obj) } } else if (jsonObject?.has(key) == true && jsonObject?.get(key) is JSONArray) { BLog.LOGE("jsonObjLog $pkey JSONArray in $key >> ${jsonObject?.getJSONArray(key)}") var array = jsonObject?.getJSONArray(key) for ( i in 0..<(array?.length() ?: 0)) { - var child = array?.getJSONObject(i) - child?.keys()?.forEach { - jsonObjLog(key,it,child) + if (array?.get(i) is String) { + + } else if (array?.get(i) is JSONObject) { + var child = array?.getJSONObject(i) + child?.keys()?.forEach { +// jsonObjLog(key, it, child) + } + } else if (array?.get(i) is JSONArray) { + BLog.LOGE("jsonObjLog $pkey JSONArray in $key >> ${array?.getJSONArray(i)}") } } } else { @@ -708,43 +715,96 @@ internal class LauncherHome : Fragment() { } } - fun ytChannel(dom: String) { + + + fun ytChannel(doc: Document) { + BLog.LOGE("ytChannel >>>>> doc${doc.title()}") try { + doc.getElementsByTag("script").forEach { +// BLog.LOGE("ytChannel >>>>> doc ${doc.title()} find script ${it}") + if(it.html().contains("var ytInitialData", false)) {/**/ + BLog.LOGE("ytChannel >>>>> doc${doc.title()} find ytInitialData ${it} ") + var ytInitialData = it.html().split("var ytInitialData = ")[1].split("")[0].toString() +// ytInitialData = ytInitialData.replace("\\x22", "\"") +// .replace("\\x7b", "{") +// .replace("\\x7d", "}") +// .replace("\\x5b", "[") +// .replace("\\x5d", "]") + BLog.LOGE("ytChannel >>>>> doc${doc.title()} find ytInitialData ${ytInitialData} ") + var tempJSONObject : JSONObject? = null + JSONObject(ytInitialData).apply{ + tempJSONObject = this + val root = Gson().fromJson(tempJSONObject.toString(), Root::class.java) + BLog.LOGE("ytChannel >>>>> doc root ${Gson().toJson(root)} apply ") + (if (root?.contents?.singleColumnBrowseResultsRenderer?.tabs?.size ?: 0 > 0) { - val parserFactory = XmlPullParserFactory.newInstance() - val kXmlParser = parserFactory.newPullParser() - val stream: InputStream = - ByteArrayInputStream(dom.toByteArray(Charset.forName("utf-8"))) - kXmlParser.setInput(stream, "utf-8") - var eventType = kXmlParser.eventType - var done = false + BLog.LOGE("ytChannel >>>>> doc singleColumnBrowseResultsRenderer apply ") + root?.contents?.singleColumnBrowseResultsRenderer?.tabs?.forEach { + it.tabRenderer?.content?.sectionListRenderer?.contents?.forEach { + BLog.LOGE("ytChannel >>>>> doc sectionListRenderer?.contents ${Gson().toJson(it)} apply ") + it.shelfRenderer?.content?.verticalListRenderer?.items?.forEach { + BLog.LOGE("ytChannel >>>>> doc verticalListRenderer?.items ${Gson().toJson(it)} apply ") + (it.compactVideoRenderer as? RssDataItem)?.let { + if(it.pubDate() >= (System.currentTimeMillis() - 1000L * 60L * 60L * 24 * 3)) { + rssSet.put(it.originPage(), it) + BLog.LOGE("ytChannel >>>>> doc RssDataItem ${Gson().toJson(it)} apply ") + } + } + } + } + } + } else { + BLog.LOGE("ytChannel >>>>> doc twoColumnBrowseResultsRenderer apply ") + root?.contents?.twoColumnBrowseResultsRenderer?.tabs?.forEach { + it.tabRenderer?.content?.sectionListRenderer?.contents?.forEach { + BLog.LOGE("ytChannel >>>>> doc sectionListRenderer?.contents ${Gson().toJson(it)} apply ") + it.itemSectionRenderer?.contents?.forEach { + BLog.LOGE("ytChannel >>>>> doc itemSectionRenderer?.items ${Gson().toJson(it)} apply ") + it.shelfRenderer?.content?.horizontalListRenderer?.items?.forEach { + (it.gridVideoRenderer as? RssDataItem)?.let { + if(it.pubDate() >= (System.currentTimeMillis() - 1000L * 60L * 60L * 24 * 3)) { + rssSet.put(it.originPage(), it) + BLog.LOGE("ytChannel >>>>> doc RssDataItem ${Gson().toJson(it)} apply ") + } + } + } + } + } + } + }).apply { + BLog.LOGE("ytChannel >>>>> doc${doc.title()} apply ") +// rssList.clear() +// rssSet.forEach { k,v -> +// rssList.add(element = v) +// }.apply { -// eventType이 End_Document가 아니고 done이 false일 경우 반복 - while(eventType != XmlPullParser.END_DOCUMENT) { - // 이름을 저장하기 위한 변수 - var name: String? = null - // eventType이 START_TAG인 경우 - if(eventType == XmlPullParser.START_TAG) { - // getName() 메서드를 사용해 현재 요소(태그)의 이름을 반환 - name = kXmlParser.name - BLog.LOGE("kXmlParser.name >> ${kXmlParser.name}") - BLog.LOGE("kXmlParser.name >> ${kXmlParser.text}") - BLog.LOGE("kXmlParser.name >> ${kXmlParser.attributeCount}") -// if(name == "city") { -// // getAttributeValue를 사용해 첫 번째 매개변수 및 -// // 두 번째 매개변수로 식별된 특성 값 획득 -// cityView.text = kXmlParser.getAttributeValue(null, "name") -// } else if(name == "temperature") { -// temperatureView.text = kXmlParser.getAttributeValue(null, "value") -// } + if(rssUrls.size > 0) { + try { + val dddd = rssUrls.removeFirst() + doWebPare(dddd) + } catch (e : Exception) { } + } + rssList.clear() + rssSet.forEach { k,v -> + rssList.add(element = v) + } + if(rssUrls.size < 1 && feddsUrls.size < 1) { + rssList.sortByDescending { it.pubDate() } + Handler(Looper.getMainLooper()).post { + mRssAdapter.updateData(rssList) + } + } + } + } } - // 다음 이벤트로 넘기기 - eventType = kXmlParser.next() } } catch(e: Exception) { } } + + + /* gestures on root view */ @SuppressLint("ClickableViewAccessibility") private fun rootViewGestures() { diff --git a/app/src/main/kotlin/rasel/lunar/launcher/todos/LinearLayoutManagerWrapper.kt b/app/src/main/kotlin/rasel/lunar/launcher/todos/LinearLayoutManagerWrapper.kt new file mode 100644 index 0000000..31a6c42 --- /dev/null +++ b/app/src/main/kotlin/rasel/lunar/launcher/todos/LinearLayoutManagerWrapper.kt @@ -0,0 +1,15 @@ +package rasel.lunar.launcher.todos + +import android.content.Context +import android.util.AttributeSet +import androidx.recyclerview.widget.LinearLayoutManager + +class LinearLayoutManagerWrapper : LinearLayoutManager { + constructor(context: Context) : super(context) + constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun supportsPredictiveItemAnimations(): Boolean { + return false + } +} diff --git a/app/src/main/kotlin/rasel/lunar/launcher/todos/RssDataItem.kt b/app/src/main/kotlin/rasel/lunar/launcher/todos/RssDataItem.kt new file mode 100644 index 0000000..462b5e5 --- /dev/null +++ b/app/src/main/kotlin/rasel/lunar/launcher/todos/RssDataItem.kt @@ -0,0 +1,17 @@ +package rasel.lunar.launcher.todos + +enum class RssDataType { + NO_DATA, + YOUTUBE, + NewsFeed + +} + +interface RssDataItem { + fun title() : String + fun thumbnailUrl() : String + fun originPage() : String + fun description() : String + fun pubDate() : Long + fun category() : RssDataType +} \ No newline at end of file diff --git a/app/src/main/kotlin/rasel/lunar/launcher/todos/RssFeedsParser.kt b/app/src/main/kotlin/rasel/lunar/launcher/todos/RssFeedsParser.kt new file mode 100644 index 0000000..96a48c2 --- /dev/null +++ b/app/src/main/kotlin/rasel/lunar/launcher/todos/RssFeedsParser.kt @@ -0,0 +1,186 @@ +package rasel.lunar.launcher.todos + +import android.util.Xml +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException +import java.io.InputStream +import java.net.URL +import java.text.SimpleDateFormat +import java.util.Locale + + +object RssFeedsParser { + var parseDateFormat: SimpleDateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH) + + fun getFeeds(url : String) : List { + var returnList = mutableListOf() + try { + returnList.addAll(parse(getInputStream(url)!!)) + } catch (e : Exception) { + e.printStackTrace() + } + return returnList + } + + private fun getInputStream(link: String?): InputStream? { + return try { + val url = URL(link) + url.openConnection().getInputStream() + } catch (ioException: IOException) { + ioException.printStackTrace() + null + } + } + + + @Throws(XmlPullParserException::class, IOException::class) + private fun parse(inputStream: InputStream): List { + return inputStream.use { stream -> + val parser = Xml.newPullParser() + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) + parser.setInput(stream, null) + parser.nextTag() + readFeed(parser) + } + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun readFeed(parser: XmlPullParser): List { + parser.require(XmlPullParser.START_TAG, null, "rss") + var title: String? = null + var link: String? = null + var date = 0L + var desc : String? = null + var source : String? = null + val items: MutableList = ArrayList() + + while (parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.eventType != XmlPullParser.START_TAG) { + continue + } + + val name = parser.name + if (name == "title") { + title = readTitle(parser) + } else if (name == "link") { + link = readLink(parser) + } else if (name == "pubDate") { + try { + date = parseDateFormat.parse(readDate(parser))?.time ?: 0L + }catch (e : Exception) { + e.printStackTrace() + } + } else if (name == "description") { + desc = readDesc(parser) + } else if (name == "source") { + source = readThumbnail(parser) + } + + if (title != null && link != null) { + val item = RssFeed(title, link) + item.pubDate = date + item.source = source + item.description = desc + items.add(item) + title = null + link = null + source = null + desc = null + date = 0 + + } + } + return items + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun readLink(parser: XmlPullParser): String { + parser.require(XmlPullParser.START_TAG, null, "link") + val link = readText(parser) + parser.require(XmlPullParser.END_TAG, null, "link") + return link + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun readTitle(parser: XmlPullParser): String { + parser.require(XmlPullParser.START_TAG, null, "title") + val title = readText(parser) + parser.require(XmlPullParser.END_TAG, null, "title") + return title + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun readDate(parser: XmlPullParser): String { + parser.require(XmlPullParser.START_TAG, null, "pubDate") + val date = readText(parser) + parser.require(XmlPullParser.END_TAG, null, "pubDate") + return date + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun readDesc(parser: XmlPullParser): String { + parser.require(XmlPullParser.START_TAG, null, "description") + val link = readText(parser) + parser.require(XmlPullParser.END_TAG, null, "description") + return link + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun readThumbnail(parser: XmlPullParser): String { + parser.require(XmlPullParser.START_TAG, null, "source") + val link = readText(parser) + parser.require(XmlPullParser.END_TAG, null, "source") + return link + } + + + @Throws(IOException::class, XmlPullParserException::class) + private fun readText(parser: XmlPullParser): String { + var result = "" + if (parser.next() == XmlPullParser.TEXT) { + result = parser.text + parser.nextTag() + } + return result + } +} + +class RssFeed : RssDataItem{ + + var title : String? = "" + var link : String? = "" + var guid : String? = "" + var description : String? = "" + var pubDate : Long = 0L + var source : String? = "" + + constructor(title: String?,link: String?) { + this.link = link + this.title = title + } + + override fun title(): String { + return title ?: "" + } + + override fun thumbnailUrl(): String { + return source ?: "" + } + + override fun originPage(): String { + return link ?: "" + } + + override fun description(): String { + return description ?: "" + } + + override fun pubDate(): Long { + return pubDate + } + + override fun category(): RssDataType { + return RssDataType.NewsFeed + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/rasel/lunar/launcher/todos/RssItemAdapter.kt b/app/src/main/kotlin/rasel/lunar/launcher/todos/RssItemAdapter.kt index 8e7972b..2f231c7 100644 --- a/app/src/main/kotlin/rasel/lunar/launcher/todos/RssItemAdapter.kt +++ b/app/src/main/kotlin/rasel/lunar/launcher/todos/RssItemAdapter.kt @@ -24,7 +24,9 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.net.Uri +import android.text.Html import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat.startActivity import androidx.core.net.toUri @@ -39,14 +41,15 @@ import rasel.lunar.launcher.databinding.ListItemWithBinding import rasel.lunar.launcher.home.RssItem import rasel.lunar.launcher.home.RssTagItem import rasel.lunar.launcher.utils.BLog +import rasel.lunar.launcher.utils.Hashids import rasel.lunar.launcher.utils.RecentSmsLog import java.text.SimpleDateFormat import java.util.Date internal class RssItemAdapter ( - private val smsList: ArrayList, - private val context: Context) : RecyclerView.Adapter() { + private val smsList: ArrayList, + private val context: Context) : RecyclerView.Adapter() { override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): RssTag { val binding = ListItemWithBinding.inflate(LayoutInflater.from(viewGroup.context), viewGroup, false) @@ -57,42 +60,48 @@ internal class RssItemAdapter ( return smsList.size } + val dateFormat = SimpleDateFormat("hh:mm / yy - MM - dd") @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: RssTag, position: Int) { val todo = smsList[position] + BLog.LOGE("rssitem >>>> ${Gson().toJson(todo)}") + holder.view.title.text = "${todo.title()}" + holder.view.desc.text = Html.fromHtml("${todo.description()}") + holder.view.date.text = "${dateFormat.format(Date(todo.pubDate()))}" + if(todo.thumbnailUrl()?.length ?: 0 > 6) { + Picasso.get().load(todo.thumbnailUrl().toUri()).into(holder.view.circlePreview) + holder.view.circlePreview.visibility = View.VISIBLE + } else { + holder.view.circlePreview.visibility = View.GONE + } - holder.view.itemText.text = "\u25CF ${Gson().toJson(todo)}" - - Picasso.get().load(todo.image.toUri()).into(holder.view.circlePreview) - BLog.LOGE("holder.view.itemText.text >>> ${holder.view.itemText.text}") - holder.view.itemText.setOnLongClickListener { + holder.view.root.setOnLongClickListener { val clipBoard = lActivity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipBoard.setPrimaryClip(ClipData.newPlainText("", todo.image)) + clipBoard.setPrimaryClip(ClipData.newPlainText("", todo.thumbnailUrl())) true } } - inner class RssTag(var view: ListItemWithBinding) : RecyclerView.ViewHolder(view.root) - - fun updateData(newList: List) { - val diffUtilResult = DiffUtil.calculateDiff(RssItemDiffUtil(smsList, newList)) - diffUtilResult.dispatchUpdatesTo(this) + fun updateData(newList: List) { + DiffUtil.calculateDiff(RssItemDiffUtil(smsList, newList)).dispatchUpdatesTo(this) } } +class RssTag(var view: ListItemWithBinding) : RecyclerView.ViewHolder(view.root) + internal class RssItemDiffUtil( - private val oldList: List, private val newList: List + private val oldList: List, private val newList: List ) : DiffUtil.Callback() { override fun getOldListSize(): Int = oldList.size override fun getNewListSize(): Int = newList.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - oldList[oldItemPosition].pageLink == newList[newItemPosition].pageLink + oldList[oldItemPosition].originPage() == newList[newItemPosition].originPage() override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - oldList[oldItemPosition].pageLink == newList[newItemPosition].pageLink + oldList[oldItemPosition].originPage() == newList[newItemPosition].originPage() } diff --git a/app/src/main/kotlin/rasel/lunar/launcher/todos/SmsLogsAdapter.kt b/app/src/main/kotlin/rasel/lunar/launcher/todos/SmsLogsAdapter.kt index 856052b..06f89d4 100644 --- a/app/src/main/kotlin/rasel/lunar/launcher/todos/SmsLogsAdapter.kt +++ b/app/src/main/kotlin/rasel/lunar/launcher/todos/SmsLogsAdapter.kt @@ -38,7 +38,7 @@ import java.util.Date internal class SmsLogsAdapter( private val smsList: ArrayList, - private val context: Context) : RecyclerView.Adapter() { + private val context: Context) : RecyclerView.Adapter() { private val currentFragment = lActivity!!.supportFragmentManager.findFragmentById(R.id.mainFragmentsContainer) @@ -93,7 +93,7 @@ internal class SmsLogsAdapter( } - inner class SmsLogHolder(var view: ListItemBinding) : RecyclerView.ViewHolder(view.root) + fun updateData(newList: List) { val diffUtilResult = DiffUtil.calculateDiff(SmsDiffUtil(smsList, newList)) @@ -140,7 +140,7 @@ internal class SmsLogsAdapter( } } - +class SmsLogHolder(var view: ListItemBinding) : RecyclerView.ViewHolder(view.root) internal class SmsDiffUtil( private val oldList: List, private val newList: List ) : DiffUtil.Callback() { diff --git a/app/src/main/kotlin/rasel/lunar/launcher/todos/YoutubeData.kt b/app/src/main/kotlin/rasel/lunar/launcher/todos/YoutubeData.kt index 9fdd434..8ec343e 100644 --- a/app/src/main/kotlin/rasel/lunar/launcher/todos/YoutubeData.kt +++ b/app/src/main/kotlin/rasel/lunar/launcher/todos/YoutubeData.kt @@ -1,8 +1,10 @@ package rasel.lunar.launcher.todos +import org.json.JSONObject +import rasel.lunar.launcher.utils.BLog +import rasel.lunar.launcher.utils.beforeDay +import java.util.Date -class YoutubeData { -} // import com.fasterxml.jackson.databind.ObjectMapper; // version 2.11.1 // import com.fasterxml.jackson.annotation.JsonProperty; // version 2.11.1 @@ -24,6 +26,7 @@ class AccessibilityContext { class AccessibilityData { var label: String? = null var accessibilityData: AccessibilityData? = null + var publishedTimeText : PublishedTimeText? = null } class Action { @@ -292,20 +295,22 @@ class ConnectionErrorMicrophoneLabel { var runs: ArrayList? = null } -class Content { +class Content : Content14() { var sectionListRenderer: SectionListRenderer? = null var horizontalListRenderer: HorizontalListRenderer? = null var pageHeaderViewModel: PageHeaderViewModel? = null + var verticalListRenderer: HorizontalListRenderer? = null var listViewModel: ListViewModel? = null } -class Content14 { +open class Content14 { var itemSectionRenderer: ItemSectionRenderer? = null var continuationItemRenderer: ContinuationItemRenderer? = null var channelFeaturedContentRenderer: ChannelFeaturedContentRenderer? = null var shelfRenderer: ShelfRenderer? = null var reelShelfRenderer: ReelShelfRenderer? = null var twoColumnBrowseResultsRenderer: TwoColumnBrowseResultsRenderer? = null + var singleColumnBrowseResultsRenderer : TwoColumnBrowseResultsRenderer? = null } class ContentMetadataViewModel { @@ -497,21 +502,21 @@ class GridChannelRenderer { var trackingParams: String? = null } -class GridVideoRenderer { - var videoId: String? = null - var thumbnail: Thumbnail? = null - var title: Title? = null - var publishedTimeText: PublishedTimeText? = null - var navigationEndpoint: NavigationEndpoint? = null +class GridVideoRenderer : VideoRenderer() { +// var videoId: String? = null +// var thumbnail: Thumbnail? = null +// var title: Title? = null +// var publishedTimeText: PublishedTimeText? = null +// var navigationEndpoint: NavigationEndpoint? = null var badges: ArrayList? = null - var ownerBadges: ArrayList? = null - var trackingParams: String? = null - var menu: Menu? = null - var thumbnailOverlays: ArrayList? = null - var viewCountText: ViewCountText? = null - var shortViewCountText: ShortViewCountText? = null +// var ownerBadges: ArrayList? = null +// var trackingParams: String? = null +// var menu: Menu? = null +// var thumbnailOverlays: ArrayList? = null +// var viewCountText: ViewCountText? = null +// var shortViewCountText: ShortViewCountText? = null var richThumbnail: RichThumbnail? = null - var shortBylineText: ShortBylineText? = null +// var shortBylineText: ShortBylineText? = null } class Header { @@ -625,10 +630,11 @@ class Item { var menuNavigationItemRenderer: MenuNavigationItemRenderer? = null var reelItemRenderer: ReelItemRenderer? = null var compactLinkRenderer: CompactLinkRenderer? = null + var compactVideoRenderer : VideoRenderer? = null } class ItemSectionRenderer { - var contents: ArrayList? = null + var contents: ArrayList? = null var trackingParams: String? = null var sectionIdentifier: String? = null var targetId: String? = null @@ -1042,6 +1048,7 @@ class Properties { class PublishedTimeText { var simpleText: String? = null + var runs : ArrayList? = null } class QoeLoggingContext { @@ -1110,15 +1117,15 @@ class RichThumbnail { } class Root { - var responseContext: ResponseContext? = null +// var responseContext: ResponseContext? = null var contents: Content14? = null var header: Header? = null var metadata: Metadata? = null - var trackingParams: String? = null - var topbar: Topbar? = null +// var trackingParams: String? = null +// var topbar: Topbar? = null var microformat: Microformat? = null - var onResponseReceivedActions: ArrayList? = null - var frameworkUpdates: FrameworkUpdates? = null +// var onResponseReceivedActions: ArrayList? = null +// var frameworkUpdates: FrameworkUpdates? = null } class Run { @@ -1368,7 +1375,7 @@ class Text { } class Thumbnail { - var thumbnails: ArrayList? = null + var thumbnails: ArrayList? = null var isOriginalAspectRatio: Boolean = false } @@ -1556,7 +1563,7 @@ class VideoCountText { var runs: ArrayList? = null } -class VideoRenderer { +open class VideoRenderer : RssDataItem { var videoId: String? = null var thumbnail: Thumbnail? = null var title: Title? = null @@ -1576,6 +1583,70 @@ class VideoRenderer { var channelThumbnailSupportedRenderers: ChannelThumbnailSupportedRenderers? = null var thumbnailOverlays: ArrayList? = null var avatar: Avatar? = null + override fun title(): String { + return title?.runs?.first()?.text ?: title?.accessibility?.label ?: "" + } + + override fun thumbnailUrl(): String { + return thumbnail?.thumbnails?.first()?.url ?: "" + } + + override fun originPage(): String { + return if (navigationEndpoint?.watchEndpoint?.videoId?.length ?: 0 > 3) { + "https://www.youtube.com/watch?v=".plus(navigationEndpoint?.watchEndpoint?.videoId) + } else if (navigationEndpoint?.commandMetadata?.webCommandMetadata?.url?.length ?: 0 > 3 ) { + "https://www.youtube.com".plus(navigationEndpoint?.commandMetadata?.webCommandMetadata?.url) + } else { + "https://youtube.com" + } + } + + override fun description(): String { + return "" + } + + override fun pubDate(): Long { + var date = Date() + var dateTime = date.time + var before = 0 + try { + var targetDate = publishedTimeText?.simpleText ?: publishedTimeText?.runs?.first()?.text ?: "" + if (targetDate?.length ?: 0 > 1) { + BLog.LOGE("targetDate >>>> ${targetDate}") + var dateDesc = targetDate +// dateDesc = dateDesc!!.replace("스트리밍 시간:","").trim() + dateDesc = dateDesc!!.split(" 전")[0].trim() + val dayString = dateDesc.replace("[^0-9]".toRegex(), "") + before = dayString.toInt() + BLog.LOGE("targetDate >>>> ${before}") + if (dateDesc.contains("년")) { + before = 365 * before + dateTime = beforeDay(date, before) + } else if (dateDesc.contains("월")) { + before = 30 * before + dateTime = beforeDay(date, before) + } else if (dateDesc.contains("주")) { + before = 7 * before + dateTime = beforeDay(date, before) + } else if (dateDesc.contains("일")) { + dateTime = beforeDay(date, before) + } else if (dateDesc.contains("시간")) { + dateTime = dateTime.minus(before.times(1000L * 60L * 60L)) + } else if (dateDesc.contains("분")) { + dateTime = dateTime.minus(before.times(1000L * 60L)) + } + } + }catch (e : Exception) { + + } + + + return dateTime + } + + override fun category(): RssDataType { + return RssDataType.YOUTUBE + } } class ViewCountText { @@ -1667,3 +1738,15 @@ class YtConfigData { var rootVisualElementType: Int = 0 } +class YoutubeData { + // var responseContext: ResponseContext? = null + var contents: JSONObject? = null + var header: JSONObject? = null + var metadata: JSONObject? = null + // var trackingParams: String? = null +// var topbar: Topbar? = null + var microformat: JSONObject? = null +// var onResponseReceivedActions: ArrayList? = null +// var frameworkUpdates: FrameworkUpdates? = null + +} \ No newline at end of file diff --git a/app/src/main/kotlin/rasel/lunar/launcher/utils/DataManager.kt b/app/src/main/kotlin/rasel/lunar/launcher/utils/DataManager.kt index c031eea..d82a2f5 100644 --- a/app/src/main/kotlin/rasel/lunar/launcher/utils/DataManager.kt +++ b/app/src/main/kotlin/rasel/lunar/launcher/utils/DataManager.kt @@ -465,3 +465,86 @@ fun getContactId(contentResolver: ContentResolver, phoneNumber: String?): String return contactName } + + +class NewsFeedsGetter : Worker { + + constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) { + + } + + + + + @SuppressLint("RestrictedApi") + override fun doWork(): Result { + BLog.LOGE("phNumber == onStart") + var dateParam = beforeDay(Date(),3).toString() + val managedCursor = lActivity?.contentResolver?.query( + Telephony.Sms.CONTENT_URI, arrayOf( + Telephony.Sms.THREAD_ID, + Telephony.Sms.ADDRESS, + Telephony.Sms.TYPE, + Telephony.Sms.DATE, + Telephony.Sms.DATE_SENT, + Telephony.Sms.BODY, + Telephony.Sms.PERSON, + ), Telephony.Sms.DATE + "> ${dateParam}", null, Telephony.Sms.DEFAULT_SORT_ORDER) + if (managedCursor != null && managedCursor.isClosed == false) { + try { + smsList.clear() + val tid = managedCursor.getColumnIndex(Telephony.Sms.THREAD_ID) + val address = managedCursor.getColumnIndex(Telephony.Sms.ADDRESS) + val type = managedCursor.getColumnIndex(Telephony.Sms.TYPE) + val date = managedCursor.getColumnIndex(Telephony.Sms.DATE) + val sendDate = managedCursor.getColumnIndex(Telephony.Sms.DATE_SENT) + val bodyIdx = managedCursor.getColumnIndex(Telephony.Sms.BODY) + val name = managedCursor.getColumnIndex(Telephony.Sms.PERSON) + while (managedCursor.moveToNext()) { + val id = managedCursor.getString(tid) // mobile number + val phNumber = managedCursor.getString(address) // mobile number + val callType = managedCursor.getString(type) // call type + val reciveDate = managedCursor.getString(date) // call date + val sendedDate = managedCursor.getString(sendDate) // call date + val smsBody = managedCursor.getString(bodyIdx).replace("\n"," ") + val callerName = managedCursor.getString(name) + + var dir: String = "" + val dircode = callType.toInt() + when (dircode) { + Telephony.Sms.MESSAGE_TYPE_ALL -> {dir = "MESSAGE_TYPE_ALL"} + Telephony.Sms.MESSAGE_TYPE_INBOX -> {dir = "MESSAGE_TYPE_INBOX"} + Telephony.Sms.MESSAGE_TYPE_SENT -> {dir = "MESSAGE_TYPE_SENT"} + Telephony.Sms.MESSAGE_TYPE_DRAFT -> {dir = "MESSAGE_TYPE_DRAFT"} + Telephony.Sms.MESSAGE_TYPE_OUTBOX -> {dir = "MESSAGE_TYPE_OUTBOX"} + Telephony.Sms.MESSAGE_TYPE_FAILED -> {dir = "MESSAGE_TYPE_FAILED"} + Telephony.Sms.MESSAGE_TYPE_QUEUED -> {dir = "MESSAGE_TYPE_QUEUED"} + } + var log = RecentSmsLog( + phNumber, + dir, + reciveDate, + sendedDate, + smsBody, + callerName ?: "" + ) + log.id = id + log.isMms = false +// BLog.LOGE("RecentSmsGetter resultData put ${phNumber +"_"+ reciveDate} >>> ${log.toJson()}") + log.sender = getContactName(applicationContext.contentResolver,phNumber) ?: "" + smsList.add(log) + + } + } catch (e: Exception) { + + } finally { + managedCursor.close() + } + } + if (lActivity?.contentResolver != null) { + TestQueryHelper(lActivity?.contentResolver!!).query() + } + return Result.success() + } + +} diff --git a/app/src/main/kotlin/rasel/lunar/launcher/utils/HashUtils.kt b/app/src/main/kotlin/rasel/lunar/launcher/utils/HashUtils.kt new file mode 100644 index 0000000..7b9ad42 --- /dev/null +++ b/app/src/main/kotlin/rasel/lunar/launcher/utils/HashUtils.kt @@ -0,0 +1,310 @@ +package rasel.lunar.launcher.utils + +import java.lang.Long.toHexString + +class Hashids(salt: String = defaultSalt, minHashLength: Int = defaultMinimalHashLength, alphabet: String = defaultAlphabet) { + companion object { + const val defaultSalt = "" + const val defaultMinimalHashLength = 0 + const val defaultAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" + const val defaultSeparators = "cfhistuCFHISTU" + const val minimalAlphabetLength = 16 + const val separatorDiv = 3.5 + const val guardDiv = 12 + + const val version = "1.1.0" + + private const val emptyString = "" + private const val space = " " + private const val maxNumber = 9007199254740992 + } + + private val finalSalt = whatSalt(salt) + private val finalHashLength = whatHashLength(minHashLength) + private val alphabetSeparatorsAndGuards = calculateAlphabetAndSeparators(alphabet) + private val finalAlphabet = alphabetSeparatorsAndGuards.alphabet + private val finalSeparators = alphabetSeparatorsAndGuards.separators + private val finalGuards = alphabetSeparatorsAndGuards.guards + + + /** + * Encodes numbers to string + * + * @param numbers the numbers to encode + * @return The encoded string + */ + fun encode(vararg numbers: Long): String = when { + numbers.isEmpty() -> emptyString + numbers.any { it > maxNumber } -> throw IllegalArgumentException("Number can not be greater than ${maxNumber}L") + else -> { + val numbersHash = numbers.indices + .map { (numbers[it] % (it + 100)).toInt() } + .sum() + + val initialCharacter = finalAlphabet.toCharArray()[numbersHash % finalAlphabet.length] + val (encodedString, encodingAlphabet) = initialEncode(numbers.asList(), finalSeparators.toCharArray(), initialCharacter.toString(), 0, finalAlphabet, initialCharacter.toString()) + val tempReturnString = addGuardsIfNecessary(encodedString, numbersHash) + + val halfLength = finalAlphabet.length / 2 + ensureMinimalLength(halfLength, encodingAlphabet, tempReturnString) + } + } + + /** + * Decodes string to numbers + * + * @param hash the encoded string + * @return Decoded numbers + */ + fun decode(hash: String): LongArray = when { + hash.isEmpty() -> longArrayOf() + else -> { + val guardsRegex = "[$finalGuards]".toRegex() + val hashWithSpacesInsteadOfGuards = hash.replace(guardsRegex, space) + val initialSplit = hashWithSpacesInsteadOfGuards.split(space) + + val (lottery, hashBreakdown) = extractLotteryCharAndHashArray(initialSplit) + val returnValue = unhashSubHashes(hashBreakdown.iterator(), lottery, mutableListOf(), finalAlphabet) + + when { + encode(*returnValue) != hash -> longArrayOf() + else -> returnValue + } + } + } + + private fun guardIndex(numbersHash: Int, returnString: String, index: Int): Int = (numbersHash + returnString.toCharArray()[index].toInt()) % finalGuards.length + + /** + * Encoded hex string to string + * + * @param hex the hex string to encode + * @return The encoded string + */ + fun encodeHex(hex: String): String = when { + !hex.matches("^[0-9a-fA-F]+$".toRegex()) -> emptyString + else -> { + val toEncode = "[\\w\\W]{1,12}".toRegex().findAll(hex) + .map { it.groupValues } + .flatten() + .map { it.toLong(16) } + .toList() + .toLongArray() + encode(*toEncode) + } + } + + /** + * Decodes string to hex numbers string + * + * @param hash the encoded string + * @return decoded hex numbers string + */ + fun decodeHex(hash: String): String = decode(hash) + .map { toHexString(it).substring(1) } + .toString() + + private fun whatSalt(aSalt: String) = when { + aSalt.isEmpty() -> defaultSalt + else -> aSalt + } + + private fun whatHashLength(aLength: Int) = when { + aLength > 0 -> aLength + else -> defaultMinimalHashLength + } + + private fun calculateAlphabetAndSeparators(userAlphabet: String): AlphabetAndSeparators { + val uniqueAlphabet = unique(userAlphabet) + when { + uniqueAlphabet.length < minimalAlphabetLength -> throw IllegalArgumentException("alphabet must contain at least $minimalAlphabetLength unique characters") + uniqueAlphabet.contains(space) -> throw IllegalArgumentException("alphabet cannot contains spaces") + else -> { + val legalSeparators = defaultSeparators.toSet().intersect(uniqueAlphabet.toSet()) + val alphabetWithoutSeparators = uniqueAlphabet.toSet().minus(legalSeparators).joinToString(emptyString) + val shuffledSeparators = consistentShuffle(legalSeparators.joinToString(emptyString), finalSalt) + val (adjustedAlphabet, adjustedSeparators) = adjustAlphabetAndSeparators(alphabetWithoutSeparators, shuffledSeparators) + + val guardCount = Math.ceil(adjustedAlphabet.length.toDouble() / guardDiv).toInt() + return if (adjustedAlphabet.length < 3) { + val guards = adjustedSeparators.substring(0, guardCount) + val seps = adjustedSeparators.substring(guardCount) + AlphabetAndSeparators(adjustedAlphabet, seps, guards) + } else { + val guards = adjustedAlphabet.substring(0, guardCount) + val alphabet = adjustedAlphabet.substring(guardCount) + AlphabetAndSeparators(alphabet, adjustedSeparators, guards) + } + } + } + } + + private fun adjustAlphabetAndSeparators(alphabetWithoutSeparators: String, shuffledSeparators: String): AlphabetAndSeparators = + if (shuffledSeparators.isEmpty() || + (alphabetWithoutSeparators.length / shuffledSeparators.length).toFloat() > separatorDiv) { + + val sepsLength = calculateSeparatorsLength(alphabetWithoutSeparators) + + if (sepsLength > shuffledSeparators.length) { + val difference = sepsLength - shuffledSeparators.length + val seps = shuffledSeparators + alphabetWithoutSeparators.substring(0, difference) + val alpha = alphabetWithoutSeparators.substring(difference) + AlphabetAndSeparators(consistentShuffle(alpha, finalSalt), seps) + } else { + val seps = shuffledSeparators.substring(0, sepsLength) + AlphabetAndSeparators(consistentShuffle(alphabetWithoutSeparators, finalSalt), seps) + } + } else { + AlphabetAndSeparators(consistentShuffle(alphabetWithoutSeparators, finalSalt), shuffledSeparators) + } + + private fun calculateSeparatorsLength(alphabet: String): Int = when (val s = Math.ceil(alphabet.length / separatorDiv).toInt()) { + 1 -> 2 + else -> s + } + + private fun unique(input: String) = input.toSet().joinToString(emptyString) + + private fun addGuardsIfNecessary(encodedString: String, numbersHash: Int): String = + if (encodedString.length < finalHashLength) { + val guard0 = finalGuards.toCharArray()[guardIndex(numbersHash, encodedString, 0)] + val retString = guard0 + encodedString + + if (retString.length < finalHashLength) { + val guard2 = finalGuards.toCharArray()[guardIndex(numbersHash, retString, 2)] + retString + guard2 + } else { + retString + } + } else { + encodedString + } + + private fun extractLotteryCharAndHashArray(initialSplit: List): Pair> { + val separatorsRegex = "[$finalSeparators]".toRegex() + val i = when { + initialSplit.size == 2 || initialSplit.size == 3 -> 1 + else -> 0 + } + val ithElementOfSplit = initialSplit[i] + + val lotteryChar = ithElementOfSplit.first() + val finalBreakdown = ithElementOfSplit + .substring(1) + .replace(separatorsRegex, space) + .split(space) + return Pair(lotteryChar, finalBreakdown) + } + + private tailrec fun unhashSubHashes(hashes: Iterator, lottery: Char, currentReturn: MutableList, alphabet: String): LongArray { + return when { + hashes.hasNext() -> { + val subHash = hashes.next() + val buffer = "$lottery$finalSalt$alphabet" + val newAlphabet = consistentShuffle(alphabet, buffer.substring(0, alphabet.length)) + currentReturn.add(unhash(subHash, newAlphabet)) + unhashSubHashes(hashes, lottery, currentReturn, newAlphabet) + } + else -> currentReturn.toLongArray() + } + } + + private fun hash(input: Long, alphabet: String): String = + doHash(input, alphabet.toCharArray(), HashData(emptyString, input)).hash + + private tailrec fun doHash(number: Long, alphabet: CharArray, data: HashData): HashData = when { + data.current > 0 -> { + val newHashCharacter = alphabet[(data.current % alphabet.size.toLong()).toInt()] + val newCurrent = data.current / alphabet.size + doHash(number, alphabet, HashData("$newHashCharacter${data.hash}", newCurrent)) + } + else -> data + } + + private fun unhash(input: String, alphabet: String): Long = + doUnhash(input.toCharArray(), alphabet, alphabet.length.toDouble(), 0, 0) + + private tailrec fun doUnhash(input: CharArray, alphabet: String, alphabetLengthDouble: Double, currentNumber: Long, currentIndex: Int): Long = + when { + currentIndex < input.size -> { + val position = alphabet.indexOf(input[currentIndex]) + val newNumber = currentNumber + (position * alphabetLengthDouble.pow((input.size.toDouble() - currentIndex - 1))).toLong() + doUnhash(input, alphabet, alphabetLengthDouble, newNumber, currentIndex + 1) + } + else -> currentNumber + } + + fun Double.pow(d : Double) : Double{ + return Math.pow(this,d) + } + + private fun consistentShuffle(alphabet: String, salt: String) = when { + salt.isEmpty() -> alphabet + else -> { + val initial = ShuffleData(alphabet.toList(), salt, 0, 0) + shuffle(initial, alphabet.length - 1, 1).alphabet.joinToString(emptyString) + } + } + + private tailrec fun shuffle(data: ShuffleData, currentPosition: Int, limit: Int): ShuffleData = when { + currentPosition < limit -> data + else -> { + val currentAlphabet = data.alphabet.toCharArray() + val saltReminder = data.saltReminder % data.salt.length + val asciiValue = data.salt[saltReminder].toInt() + val cumulativeValue = data.cumulative + asciiValue + val positionToSwap = (asciiValue + saltReminder + cumulativeValue) % currentPosition + currentAlphabet[positionToSwap] = currentAlphabet[currentPosition].also { + currentAlphabet[currentPosition] = currentAlphabet[positionToSwap] + } + shuffle(ShuffleData(currentAlphabet.toList(), data.salt, cumulativeValue, saltReminder + 1), currentPosition - 1, limit) + } + } + + private tailrec fun initialEncode(numbers: List, + separators: CharArray, + bufferSeed: String, + currentIndex: Int, + alphabet: String, + currentReturnString: String): Pair = when { + currentIndex < numbers.size -> { + val currentNumber = numbers[currentIndex] + val buffer = bufferSeed + finalSalt + alphabet + val nextAlphabet = consistentShuffle(alphabet, buffer.substring(0, alphabet.length)) + val last = hash(currentNumber, nextAlphabet) + + val newReturnString = if (currentIndex + 1 < numbers.size) { + val nextNumber = currentNumber % (last.toCharArray()[0].toInt() + currentIndex) + val sepsIndex = (nextNumber % separators.size).toInt() + currentReturnString + last + separators[sepsIndex] + } else { + currentReturnString + last + } + initialEncode(numbers, separators, bufferSeed, currentIndex + 1, nextAlphabet, newReturnString) + } + else -> Pair(currentReturnString, alphabet) + } + + private tailrec fun ensureMinimalLength(halfLength: Int, alphabet: String, returnString: String): String = when { + returnString.length < finalHashLength -> { + val newAlphabet = consistentShuffle(alphabet, alphabet) + val tempReturnString = newAlphabet.substring(halfLength) + returnString + newAlphabet.substring(0, halfLength) + val excess = tempReturnString.length - finalHashLength + val newReturnString = if (excess > 0) { + val position = excess / 2 + tempReturnString.substring(position, position + finalHashLength) + } else { + tempReturnString + } + ensureMinimalLength(halfLength, newAlphabet, newReturnString) + } + else -> returnString + } + +} + +private data class AlphabetAndSeparators(val alphabet: String, val separators: String, val guards: String = "") + +private data class ShuffleData(val alphabet: List, val salt: String, val cumulative: Int, val saltReminder: Int) + +private data class HashData(val hash: String, val current: Long) diff --git a/app/src/main/kotlin/rasel/lunar/launcher/utils/RssList.kt b/app/src/main/kotlin/rasel/lunar/launcher/utils/RssList.kt index 6a79081..04e2a58 100644 --- a/app/src/main/kotlin/rasel/lunar/launcher/utils/RssList.kt +++ b/app/src/main/kotlin/rasel/lunar/launcher/utils/RssList.kt @@ -5,6 +5,7 @@ import org.jsoup.nodes.Document import rasel.lunar.launcher.LauncherActivity.Companion.TEST_PAG import rasel.lunar.launcher.home.LauncherHome.Companion.listItem import rasel.lunar.launcher.home.RssItem +import java.net.URLEncoder import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -17,6 +18,16 @@ object RssList { "https://www.youtube.com/@gyeomsonisnothing", "https://www.youtube.com/@ddeunddeun" ) + val newsFeeds = arrayListOf( + "https://news.google.com/rss?hl=ko&gl=KR&ceid=KR:ko", + "https://news.google.com/rss/search?q=${URLEncoder.encode("IT")}=ko&gl=KR&ceid=KR%3Ako/", + "https://news.google.com/rss/search?q=${URLEncoder.encode("영화")}=ko&gl=KR&ceid=KR%3Ako/", + "https://news.google.com/rss/search?q=${URLEncoder.encode("음악")}=ko&gl=KR&ceid=KR%3Ako/", + "https://news.google.com/rss/search?q=${URLEncoder.encode("날씨")}=ko&gl=KR&ceid=KR%3Ako/", + "https://news.google.com/rss/search?q=${URLEncoder.encode("테크")}=ko&gl=KR&ceid=KR%3Ako/", + "https://news.google.com/rss/search?q=${URLEncoder.encode("최신")}=ko&gl=KR&ceid=KR%3Ako/", + "https://news.google.com/rss/search?q=${URLEncoder.encode("경제")}=ko&gl=KR&ceid=KR%3Ako/", + ) } object DocParserManager { diff --git a/app/src/main/res/layout/launcher_home.xml b/app/src/main/res/layout/launcher_home.xml index 474c2e4..c4ece99 100644 --- a/app/src/main/res/layout/launcher_home.xml +++ b/app/src/main/res/layout/launcher_home.xml @@ -134,7 +134,7 @@ android:overScrollMode="never" android:padding="20dp" android:scrollbars="none" - app:layoutManager="LinearLayoutManager" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/summaryChoose" diff --git a/app/src/main/res/layout/list_item_with.xml b/app/src/main/res/layout/list_item_with.xml index c0b323c..aeaee69 100644 --- a/app/src/main/res/layout/list_item_with.xml +++ b/app/src/main/res/layout/list_item_with.xml @@ -4,25 +4,41 @@ android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto"> - - + + android:scaleType="fitCenter" + android:adjustViewBounds="true" + android:layout_width="90dp" + android:layout_height="90dp"/> - + + + app:layout_constraintLeft_toRightOf="@id/circle_preview" + app:layout_constraintRight_toRightOf="parent"/> \ No newline at end of file