From 1886fcd80e132f79389eed2bbc06c53575e6cbbb Mon Sep 17 00:00:00 2001 From: LagradOst Date: Sun, 15 Aug 2021 19:38:41 +0200 Subject: [PATCH] inapp updater and intent and dubbedanime --- app/src/main/AndroidManifest.xml | 17 +- .../com/lagradost/cloudstream3/MainAPI.kt | 11 +- .../lagradost/cloudstream3/MainActivity.kt | 6 + .../animeproviders/DubbedAnimeProvider.kt | 90 +++++- .../cloudstream3/ui/APIRepository.kt | 5 +- .../cloudstream3/ui/home/HomeFragment.kt | 7 +- .../ui/settings/SettingsFragment.kt | 14 + .../cloudstream3/utils/InAppUpdater.kt | 283 ++++++++++++++++++ .../ic_baseline_notifications_active_24.xml | 5 + .../drawable/ic_baseline_system_update_24.xml | 5 + app/src/main/res/layout/fragment_home.xml | 1 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/settings.xml | 12 + 13 files changed, 438 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt create mode 100644 app/src/main/res/drawable/ic_baseline_notifications_active_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_system_update_24.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a083008d..398a0d5e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,11 +30,24 @@ android:supportsPictureInPicture="true"> - - + + + + + + + + + + + + + + + ? { + open fun search(query: String): List? { return null } - open fun quickSearch(query: String): ArrayList? { + open fun quickSearch(query: String): List? { return null } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index ba023c3b..39e08daa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -32,8 +32,10 @@ import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos import com.lagradost.cloudstream3.utils.Event +import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.fragment_result.* +import kotlin.concurrent.thread const val VLC_PACKAGE = "org.videolan.vlc" const val VLC_INTENT_ACTION_RESULT = "org.videolan.vlc.player.result" @@ -295,6 +297,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { createISO() }*/ handleAppIntent(intent) + + thread { + runAutoUpdate() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/DubbedAnimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/DubbedAnimeProvider.kt index 21d1df12..e9318f84 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/DubbedAnimeProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/DubbedAnimeProvider.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.module.kotlin.readValue import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.unixTime +import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.getQualityFromName import org.jsoup.Jsoup @@ -17,6 +18,8 @@ class DubbedAnimeProvider : MainAPI() { get() = "DubbedAnime" override val hasQuickSearch: Boolean get() = true + override val hasMainPage: Boolean + get() = true override val supportedTypes: Set get() = setOf( @@ -57,6 +60,67 @@ class DubbedAnimeProvider : MainAPI() { @JsonProperty("tags") val tags: String,*/ ) + private fun parseDocumentTrending(url: String): List { + val response = khttp.get(url) + val document = Jsoup.parse(response.text) + return document.select("li > a").map { + val href = fixUrl(it.attr("href")) + val title = it.selectFirst("> div > div.cittx").text() + val poster = fixUrl(it.selectFirst("> div > div.imghddde > img").attr("src")) + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + poster, + null, + null, + EnumSet.of(DubStatus.Dubbed), + null, + null + ) + } + } + + private fun parseDocument(url: String, trimEpisode: Boolean = false): List { + val response = khttp.get(url) + val document = Jsoup.parse(response.text) + return document.select("a.grid__link").map { + val href = fixUrl(it.attr("href")) + val title = it.selectFirst("> div.gridtitlek").text() + val poster = fixUrl(it.selectFirst("> img.grid__img").attr("src")) + AnimeSearchResponse( + title, + if (trimEpisode) href.removeRange(href.lastIndexOf('/'), href.length) else href, + this.name, + TvType.Anime, + poster, + null, + null, + EnumSet.of(DubStatus.Dubbed), + null, + null + ) + } + } + + override fun getMainPage(): HomePageResponse { + val trendingUrl = "$mainUrl/xz/trending.php?_=$unixTimeMS" + val lastEpisodeUrl = "$mainUrl/xz/epgrid.php?p=1&_=$unixTimeMS" + val recentlyAddedUrl = "$mainUrl/xz/gridgrabrecent.php?p=1&_=$unixTimeMS" + //val allUrl = "$mainUrl/xz/gridgrab.php?p=1&limit=12&_=$unixTimeMS" + + val listItems = listOf( + HomePageList("Trending", parseDocumentTrending(trendingUrl)), + HomePageList("Recently Added", parseDocument(recentlyAddedUrl)), + HomePageList("Recent Releases", parseDocument(lastEpisodeUrl, true)), + // HomePageList("All", parseDocument(allUrl)) + ) + + return HomePageResponse(listItems) + } + + private fun getAnimeEpisode(slug: String, isMovie: Boolean): EpisodeInfo { val url = mainUrl + (if (isMovie) "/movies/jsonMovie" else "/xz/v3/jsonEpi") + ".php?slug=$slug&_=$unixTime" @@ -76,8 +140,8 @@ class DubbedAnimeProvider : MainAPI() { return href.replace("$mainUrl/", "") } - override fun quickSearch(query: String): ArrayList { - val url = "$mainUrl/xz/searchgrid.php?p=1&limit=12&s=$query&_=${unixTime}" + override fun quickSearch(query: String): List { + val url = "$mainUrl/xz/searchgrid.php?p=1&limit=12&s=$query&_=$unixTime" val response = khttp.get(url) val document = Jsoup.parse(response.text) val items = document.select("div.grid__item > a") @@ -111,7 +175,7 @@ class DubbedAnimeProvider : MainAPI() { return returnValue } - override fun search(query: String): ArrayList { + override fun search(query: String): List { val url = "$mainUrl/search/$query" val response = khttp.get(url) val document = Jsoup.parse(response.text) @@ -188,9 +252,8 @@ class DubbedAnimeProvider : MainAPI() { } override fun load(url: String): LoadResponse { - val slug = url.replace("$mainUrl/","") - if (getIsMovie(slug)) { - val realSlug = slug.replace("movies/", "") + if (getIsMovie(url)) { + val realSlug = url.replace("movies/", "") val episode = getAnimeEpisode(realSlug, true) val poster = episode.previewImg ?: episode.wideImg return MovieLoadResponse( @@ -205,7 +268,7 @@ class DubbedAnimeProvider : MainAPI() { null ) } else { - val response = khttp.get("$mainUrl/$slug") + val response = khttp.get(url) val document = Jsoup.parse(response.text) val title = document.selectFirst("h4").text() val descriptHeader = document.selectFirst("div.animeDescript") @@ -220,7 +283,18 @@ class DubbedAnimeProvider : MainAPI() { val img = fixUrl(document.select("div.fkimgs > img").attr("src")) return AnimeLoadResponse( - null, null, title, "$mainUrl/$slug", this.name, TvType.Anime, img, year, ArrayList(episodes), null, null, descript, + null, + null, + title, + url, + this.name, + TvType.Anime, + img, + year, + ArrayList(episodes), + null, + null, + descript, ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 7b9cba45..da39a603 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -18,8 +18,7 @@ class APIRepository(val api: MainAPI) { suspend fun load(url: String): Resource { return safeApiCall { - // remove suffix for some slugs to handle correctly - api.load(url.removeSuffix("/")) ?: throw ErrorLoadingException() + api.load(api.fixUrl(url)) ?: throw ErrorLoadingException() } } @@ -30,7 +29,7 @@ class APIRepository(val api: MainAPI) { } } - suspend fun quickSearch(query: String): Resource> { + suspend fun quickSearch(query: String): Resource> { return safeApiCall { api.quickSearch(query) ?: throw ErrorLoadingException() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 5f5e8b05..78d3234d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -122,7 +122,12 @@ class HomeFragment : Fragment() { val MAX_BREAK_COUNT = 10 while (random?.posterUrl == null) { - random = home.items.random().list.random() + try { + random = home.items.random().list.random() + } catch (e : Exception) { + // probs Collection is empty. + } + breakCount++ if (breakCount > MAX_BREAK_COUNT) { break diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index f65fe939..f8dcee90 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -1,16 +1,30 @@ package com.lagradost.cloudstream3.ui.settings import android.os.Bundle +import android.widget.Toast import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment +import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard +import kotlin.concurrent.thread class SettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { hideKeyboard() setPreferencesFromResource(R.xml.settings, rootKey) + val updatePrefrence = findPreference(getString(R.string.manual_check_update_key))!! + updatePrefrence.setOnPreferenceClickListener { + thread { + if (!requireActivity().runAutoUpdate(false)) { + activity?.runOnUiThread { + Toast.makeText(this.context, "No Update Found", Toast.LENGTH_SHORT).show() + } + } + } + return@setOnPreferenceClickListener true + } } override fun onPreferenceTreeClick(preference: Preference?): Boolean { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt new file mode 100644 index 00000000..cc70366b --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -0,0 +1,283 @@ +package com.lagradost.cloudstream3.utils + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.FileProvider +import androidx.preference.PreferenceManager +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.R +import java.io.* +import java.net.URL +import java.net.URLConnection +import kotlin.concurrent.thread + +const val UPDATE_TIME = 1000 + +class InAppUpdater { + companion object { + // === IN APP UPDATER === + data class GithubAsset( + @JsonProperty("name") val name: String, + @JsonProperty("size") val size: Int, // Size bytes + @JsonProperty("browser_download_url") val browser_download_url: String, // download link + @JsonProperty("content_type") val content_type: String, // application/vnd.android.package-archive + ) + + data class GithubRelease( + @JsonProperty("tag_name") val tag_name: String, // Version code + @JsonProperty("body") val body: String, // Desc + @JsonProperty("assets") val assets: List, + @JsonProperty("target_commitish") val target_commitish: String, // branch + ) + + data class Update( + @JsonProperty("shouldUpdate") val shouldUpdate: Boolean, + @JsonProperty("updateURL") val updateURL: String?, + @JsonProperty("updateVersion") val updateVersion: String?, + @JsonProperty("changelog") val changelog: String?, + ) + + private val mapper = JsonMapper.builder().addModule(KotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() + + private fun Activity.getAppUpdate(): Update { + try { + val url = "https://api.github.com/repos/LagradOst/CloudStream-3/releases" + val headers = mapOf("Accept" to "application/vnd.github.v3+json") + val response = + mapper.readValue>(khttp.get(url, headers = headers).text) + + val versionRegex = Regex("""(.*?((\d)\.(\d)\.(\d)).*\.apk)""") + + /* + val releases = response.map { it.assets }.flatten() + .filter { it.content_type == "application/vnd.android.package-archive" } + val found = + releases.sortedWith(compareBy { + versionRegex.find(it.name)?.groupValues?.get(2) + }).toList().lastOrNull()*/ + val found = + response.sortedWith(compareBy { release -> + release.assets.filter { it.content_type == "application/vnd.android.package-archive" } + .getOrNull(0)?.name?.let { it1 -> + versionRegex.find( + it1 + )?.groupValues?.get(2) + } + }).toList().lastOrNull() + val foundAsset = found?.assets?.getOrNull(0) + val currentVersion = packageName?.let { + packageManager.getPackageInfo(it, + 0) + } + + val foundVersion = foundAsset?.name?.let { versionRegex.find(it) } + val shouldUpdate = if (found != null && foundAsset?.browser_download_url != "" && foundVersion != null) currentVersion?.versionName?.compareTo( + foundVersion.groupValues[2] + )!! < 0 else false + return if (foundVersion != null) { + Update(shouldUpdate, foundAsset.browser_download_url, foundVersion.groupValues[2], found.body) + } else { + Update(false, null, null, null) + } + + } catch (e: Exception) { + println(e) + return Update(false, null, null, null) + } + } + + private fun Activity.downloadUpdate(url: String): Boolean { + println("DOWNLOAD UPDATE $url") + var fullResume = false // IF FULL RESUME + try { + // =================== DOWNLOAD POSTERS AND SETUP PATH =================== + val path = filesDir.toString() + + "/Download/apk/update.apk" + + // =================== MAKE DIRS =================== + val rFile = File(path) + try { + rFile.parentFile?.mkdirs() + } catch (_ex: Exception) { + println("FAILED:::$_ex") + } + val url = url.replace(" ", "%20") + + val _url = URL(url) + + val connection: URLConnection = _url.openConnection() + + var bytesRead = 0L + + // =================== STORAGE =================== + try { + if (!rFile.exists()) { + rFile.createNewFile() + } else { + rFile.delete() + rFile.createNewFile() + } + } catch (e: Exception) { + println(e) + runOnUiThread { + Toast.makeText(this, "Permission error", Toast.LENGTH_SHORT).show() + } + return false + } + + // =================== CONNECTION =================== + connection.setRequestProperty("Accept-Encoding", "identity") + connection.connectTimeout = 10000 + var clen = 0 + try { + connection.connect() + clen = connection.contentLength + println("CONTENTN LENGTH: $clen") + } catch (_ex: Exception) { + println("CONNECT:::$_ex") + _ex.printStackTrace() + } + + // =================== VALIDATE =================== + if (clen < 5000000) { // min of 5 MB + clen = 0 + } + if (clen <= 0) { // TO SMALL OR INVALID + //showNot(0, 0, 0, DownloadType.IsFailed, info) + return false + } + + // =================== SETUP VARIABLES =================== + //val bytesTotal: Long = (clen + bytesRead.toInt()).toLong() + val input: InputStream = BufferedInputStream(connection.inputStream) + val output: OutputStream = FileOutputStream(rFile, false) + var bytesPerSec = 0L + val buffer = ByteArray(1024) + var count: Int + //var lastUpdate = System.currentTimeMillis() + + while (true) { + try { + count = input.read(buffer) + if (count < 0) break + + bytesRead += count + bytesPerSec += count + output.write(buffer, 0, count) + } catch (_ex: Exception) { + println("CONNECT TRUE:::$_ex") + _ex.printStackTrace() + fullResume = true + break + } + } + + if (fullResume) { // IF FULL RESUME DELETE CURRENT AND DONT SHOW DONE + with(NotificationManagerCompat.from(this)) { + cancel(-1) + } + } + + output.flush() + output.close() + input.close() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val contentUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID + ".provider", + rFile + ) + val install = Intent(Intent.ACTION_VIEW) + install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + install.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + install.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) + install.data = contentUri + startActivity(install) + return true + } else { + val apkUri = Uri.fromFile(rFile) + val install = Intent(Intent.ACTION_VIEW) + install.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + install.setDataAndType( + apkUri, + "application/vnd.android.package-archive" + ) + startActivity(install) + return true + } + + } catch (_ex: Exception) { + println("FATAL EX DOWNLOADING:::$_ex") + return false + } + } + + fun Activity.runAutoUpdate(checkAutoUpdate: Boolean = true): Boolean { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + + if (!checkAutoUpdate || settingsManager.getBoolean(getString(R.string.auto_update_key), true) + ) { + val update = getAppUpdate() + if (update.shouldUpdate && update.updateURL != null) { + runOnUiThread { + val currentVersion = packageName?.let { + packageManager.getPackageInfo(it, + 0) + } + + val builder: AlertDialog.Builder = AlertDialog.Builder(this) + builder.setTitle("New update found!\n${currentVersion?.versionName} -> ${update.updateVersion}") + builder.setMessage("${update.changelog}") + + val context = this + builder.apply { + setPositiveButton("Update") { _, _ -> + Toast.makeText(context, "Download started", Toast.LENGTH_LONG).show() + thread { + val downloadStatus = context.downloadUpdate(update.updateURL) + if (!downloadStatus) { + runOnUiThread { + Toast.makeText(context, + "Download Failed", + Toast.LENGTH_LONG).show() + } + } /*else { + activity.runOnUiThread { + Toast.makeText(localContext, + "Downloaded APK", + Toast.LENGTH_LONG).show() + } + }*/ + } + } + + setNegativeButton("Cancel") { _, _ -> } + + if(checkAutoUpdate) { + setNeutralButton("Don't show again") { _, _ -> + settingsManager.edit().putBoolean("auto_update", false).apply() + } + } + } + builder.show() + } + return true + } + return false + } + return false + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml b/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml new file mode 100644 index 00000000..053e07ae --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_system_update_24.xml b/app/src/main/res/drawable/ic_baseline_system_update_24.xml new file mode 100644 index 00000000..7ad99c9a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_system_update_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 3651b2c1..2d4a3a61 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -87,6 +87,7 @@ android:id="@+id/home_main_poster" tools:src="@drawable/example_poster" android:layout_gravity="center" + android:scaleType="centerCrop" android:layout_width="150dp" android:layout_height="212dp" android:contentDescription="@string/home_main_poster"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 97a60fe8..781eb895 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -83,4 +83,6 @@ Font Search using providers Search using types + auto_update + manual_check_update \ No newline at end of file diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index d4273244..1dd14dbd 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -84,6 +84,18 @@ android:summaryOff="Only sends data on crashes" android:summaryOn="Sends no data" android:defaultValue="false"/> + +