diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ee4c978f2be..29d5523ff35 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -174,6 +174,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
index 8a98bd2972e..adb9d08cccd 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -9,7 +9,9 @@ import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Rect
+import android.os.Handler
import android.os.Bundle
+import android.os.Looper
import android.util.AttributeSet
import android.util.Log
import android.view.Gravity
@@ -156,6 +158,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.Event
+import com.lagradost.cloudstream3.utils.extractorApis
import com.lagradost.cloudstream3.utils.ImageLoader.loadImage
import com.lagradost.cloudstream3.utils.InAppUpdater.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
@@ -217,6 +220,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
private const val FILE_DELETE_KEY = "FILES_TO_DELETE_KEY"
const val API_NAME_EXTRA_KEY = "API_NAME_EXTRA_KEY"
+ const val EXTRA_SHARED_URL = "EXTRA_SHARED_URL"
+ const val EXTRA_SHARED_URL_ID = "EXTRA_SHARED_URL_ID"
/**
* Transient files to delete on application exit.
@@ -726,6 +731,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
broadcastIntent.setClass(this, VideoDownloadRestartReceiver::class.java)
this.sendBroadcast(broadcastIntent)
afterPluginsLoadedEvent -= ::onAllPluginsLoaded
+ sharedUrlPluginObserver?.let { afterPluginsLoadedEvent -= it }
+ sharedUrlPluginObserver = null
+ sharedUrlHandler.removeCallbacksAndMessages(null)
detachBackPressedCallback("MainActivityDefault")
super.onDestroy()
}
@@ -740,9 +748,103 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
val str = intent.dataString
loadCache()
+ if (handleSharedUrlIntent(intent)) return
handleAppIntentUrl(this, str, false, intent.extras)
}
+ private fun handleSharedUrlIntent(intent: Intent): Boolean {
+ val sharedUrl = intent.getStringExtra(EXTRA_SHARED_URL) ?: return false
+ val sharedUrlId = intent.getStringExtra(EXTRA_SHARED_URL_ID) ?: sharedUrl
+ if (!handledSharedUrlIds.add(sharedUrlId)) return true
+
+ var hasHandledSharedUrl = false
+ var timeoutRunnable: Runnable? = null
+ fun tryRouteSharedUrl(showUnsupported: Boolean): Boolean {
+ if (hasHandledSharedUrl) return true
+ hasHandledSharedUrl = routeSharedUrl(sharedUrl, showUnsupported)
+ if (hasHandledSharedUrl) timeoutRunnable?.let(sharedUrlHandler::removeCallbacks)
+ return hasHandledSharedUrl
+ }
+
+ if (tryRouteSharedUrl(showUnsupported = false)) return true
+ if (sharedLinkPluginsLoaded) {
+ tryRouteSharedUrl(showUnsupported = true)
+ return true
+ }
+
+ sharedUrlPluginObserver?.let { afterPluginsLoadedEvent -= it }
+ lateinit var observer: (Boolean) -> Unit
+ observer = {
+ main {
+ afterPluginsLoadedEvent -= observer
+ sharedUrlPluginObserver = null
+ tryRouteSharedUrl(showUnsupported = false)
+ }
+ }
+ sharedUrlPluginObserver = observer
+ afterPluginsLoadedEvent += observer
+
+ timeoutRunnable = Runnable {
+ afterPluginsLoadedEvent -= observer
+ sharedUrlPluginObserver = null
+ if (!tryRouteSharedUrl(showUnsupported = false)) {
+ tryRouteSharedUrl(showUnsupported = true)
+ }
+ }
+ sharedUrlHandler.postDelayed(timeoutRunnable, 2500)
+
+ return true
+ }
+
+ private fun routeSharedUrl(sharedUrl: String, showUnsupported: Boolean): Boolean {
+ val normalizedUrl = normalizeSharedUrl(sharedUrl)
+ val provider = APIHolder.getApiFromUrlNull(sharedUrl)
+
+ if (provider != null) {
+ loadResult(sharedUrl, provider.name, "")
+ return true
+ }
+
+ val extractor = synchronized(extractorApis) {
+ extractorApis.asReversed()
+ .firstOrNull { normalizedUrl.matchesSharedUrlBase(normalizeSharedUrl(it.mainUrl)) }
+ }
+
+ if (extractor != null) {
+ sharedUrlHandler.post {
+ navigate(
+ R.id.global_to_navigation_player,
+ GeneratorPlayer.newInstance(
+ LinkGenerator(
+ listOf(BasicLink(sharedUrl)),
+ extract = true,
+ id = sharedUrl.hashCode()
+ ),
+ 0
+ )
+ )
+ }
+ return true
+ }
+
+ if (showUnsupported) {
+ showToast(this, "Unsupported shared link", Toast.LENGTH_LONG)
+ return true
+ }
+
+ return false
+ }
+
+ private fun normalizeSharedUrl(url: String): String {
+ return url.lowercase()
+ .replace(Regex("""^(https?:)?//(www\.)?"""), "")
+ .trimEnd('/')
+ }
+
+ private fun String.matchesSharedUrlBase(baseUrl: String): Boolean {
+ return this == baseUrl || startsWith("$baseUrl/")
+ }
+
private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
hierarchy.any { it.id == destId }
@@ -811,7 +913,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
private val pluginsLock = Mutex()
+ private var sharedLinkPluginsLoaded = false
+
private fun onAllPluginsLoaded(success: Boolean = false) {
+ sharedLinkPluginsLoaded = true
ioSafe {
pluginsLock.withLock {
synchronized(allProviders) {
@@ -847,6 +952,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
lateinit var viewModel: ResultViewModel2
lateinit var syncViewModel: SyncViewModel
private var libraryViewModel: LibraryViewModel? = null
+ private var sharedUrlPluginObserver: ((Boolean) -> Unit)? = null
+ private val sharedUrlHandler = Handler(Looper.getMainLooper())
+ private val handledSharedUrlIds = mutableSetOf()
/** kinda dirty, however it signals that we should use the watch status as sync or not*/
var isLocalList: Boolean = false
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt
index ad323c7d124..6d01322f908 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt
@@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.ui.account
import android.annotation.SuppressLint
+import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.FragmentActivity
@@ -11,6 +12,8 @@ import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.CommonActivity.loadThemes
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity
+import com.lagradost.cloudstream3.MainActivity.Companion.EXTRA_SHARED_URL
+import com.lagradost.cloudstream3.MainActivity.Companion.EXTRA_SHARED_URL_ID
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ActivityAccountSelectBinding
import com.lagradost.cloudstream3.mvvm.observe
@@ -35,6 +38,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.enableEdgeToEdgeCompat
import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding
import com.lagradost.cloudstream3.utils.UIHelper.openActivity
import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat
+import java.util.UUID
class AccountSelectActivity : FragmentActivity(), BiometricCallback {
@@ -205,10 +209,35 @@ class AccountSelectActivity : FragmentActivity(), BiometricCallback {
private fun navigateToMainActivity() {
hasLoggedIn = true
// We want to propagate any intent we get here to MainActivity since this is just an intermediary
- openActivity(MainActivity::class.java, baseIntent = intent)
+ openActivity(MainActivity::class.java, baseIntent = intent.withExtractedSharedUrl())
finish() // Finish the account selection activity
}
+ private fun Intent.withExtractedSharedUrl(): Intent {
+ val url = extractSharedUrl() ?: return this
+ return Intent(this).apply {
+ putExtra(EXTRA_SHARED_URL, url)
+ putExtra(EXTRA_SHARED_URL_ID, getStringExtra(EXTRA_SHARED_URL_ID) ?: UUID.randomUUID().toString())
+ }
+ }
+
+ private fun Intent.extractSharedUrl(): String? {
+ val candidates = listOfNotNull(
+ dataString,
+ getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString(),
+ @Suppress("DEPRECATION")
+ getParcelableExtra(Intent.EXTRA_STREAM)?.toString(),
+ )
+
+ return candidates.firstNotNullOfOrNull { value ->
+ value
+ .lineSequence()
+ .flatMap { it.splitToSequence(Regex("\\s+")) }
+ .map { it.trimEnd('.', ',', ';', ':', '!', '?', ')', ']', '}', '>', '\'', '"') }
+ .firstOrNull { it.startsWith("http://") || it.startsWith("https://") }
+ }
+ }
+
override fun onAuthenticationSuccess() {
Log.i(BiometricAuthenticator.TAG, "Authentication successful in AccountSelectActivity")
}