diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index de57c05..7db277c 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -16,5 +16,12 @@ + diff --git a/android/src/main/java/dev/zoriya/omni/OmniCastOptionsProvider.kt b/android/src/main/java/dev/zoriya/omni/OmniCastOptionsProvider.kt index e2d1d7e..2e5a2b9 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniCastOptionsProvider.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniCastOptionsProvider.kt @@ -26,11 +26,7 @@ class OmniCastOptionsProvider : OptionsProvider { .setSmallIconDrawableResId( androidx.media3.session.R.drawable.media3_notification_small_icon ) - .apply { - context.packageManager.getLaunchIntentForPackage(context.packageName) - ?.component?.className - ?.let { setTargetActivityClassName(it) } - } + .setTargetActivityClassName(OmniCastTapActivity::class.java.name) .build() val mediaOptions = CastMediaOptions.Builder() diff --git a/android/src/main/java/dev/zoriya/omni/OmniCastTapActivity.kt b/android/src/main/java/dev/zoriya/omni/OmniCastTapActivity.kt new file mode 100644 index 0000000..2fc3719 --- /dev/null +++ b/android/src/main/java/dev/zoriya/omni/OmniCastTapActivity.kt @@ -0,0 +1,30 @@ +package dev.zoriya.omni + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.net.toUri + +class OmniCastTapActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val url = getSharedPreferences(OmniPlayer.OMNI_PREFS, Context.MODE_PRIVATE) + .getString(OmniPlayer.NOTIFICATION_URL_PREF, null) + val intent = url + ?.let { Intent(Intent.ACTION_VIEW, it.toUri()).setPackage(packageName) } + ?: packageManager.getLaunchIntentForPackage(packageName) + + try { + startActivity( + intent?.apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + ) + } catch (e: Throwable) { + android.util.Log.w("OmniPlayer", "could not open $url", e) + } + finish() + } +} diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt index 339c013..b9eea17 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt @@ -128,6 +128,12 @@ class OmniPlayer( val player: Player = runOnMainThreadSync { castOptions?.receiverApplicationId?.let { receiverApplicationId = it } + // the cast notification outlives the app, so OmniCastTapActivity has to find the + // url back even when nothing of ours ran in that process yet. + ctx.getSharedPreferences(OMNI_PREFS, Context.MODE_PRIVATE) + .edit() + .putString(NOTIFICATION_URL_PREF, castOptions?.notificationUrl) + .apply() val cc = try { CastContext.getSharedInstance(ctx) } catch (_: Throwable) { @@ -648,6 +654,10 @@ class OmniPlayer( // Receiver application id passed at runtime via OmniProvider's `cast` // prop; read by OmniCastOptionsProvider when the Cast SDK initializes. var receiverApplicationId: String? = null + + // `cast.notificationUrl`, read back by OmniCastTapActivity. + const val OMNI_PREFS = "dev.zoriya.omni" + const val NOTIFICATION_URL_PREF = "notificationUrl" } } diff --git a/example/App.tsx b/example/App.tsx index 1dca28b..1d032ea 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -511,7 +511,10 @@ function App(): React.JSX.Element { + + + + + + getField("receiverApplicationId"); jni::local_ref receiverApplicationId = this->getFieldValue(fieldReceiverApplicationId); + static const auto fieldNotificationUrl = clazz->getField("notificationUrl"); + jni::local_ref notificationUrl = this->getFieldValue(fieldNotificationUrl); return CastOptions( - receiverApplicationId != nullptr ? std::make_optional(receiverApplicationId->toStdString()) : std::nullopt + receiverApplicationId != nullptr ? std::make_optional(receiverApplicationId->toStdString()) : std::nullopt, + notificationUrl != nullptr ? std::make_optional(notificationUrl->toStdString()) : std::nullopt ); } @@ -45,12 +48,13 @@ namespace margelo::nitro::omni { */ [[maybe_unused]] static jni::local_ref fromCpp(const CastOptions& value) { - using JSignature = JCastOptions(jni::alias_ref); + using JSignature = JCastOptions(jni::alias_ref, jni::alias_ref); static const auto clazz = javaClassStatic(); static const auto create = clazz->getStaticMethod("fromCpp"); return create( clazz, - value.receiverApplicationId.has_value() ? jni::make_jstring(value.receiverApplicationId.value()) : nullptr + value.receiverApplicationId.has_value() ? jni::make_jstring(value.receiverApplicationId.value()) : nullptr, + value.notificationUrl.has_value() ? jni::make_jstring(value.notificationUrl.value()) : nullptr ); } }; diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/CastOptions.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/CastOptions.kt index a8b165b..4cb72d4 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/CastOptions.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/CastOptions.kt @@ -20,7 +20,10 @@ import java.util.Objects data class CastOptions( @DoNotStrip @Keep - val receiverApplicationId: String? + val receiverApplicationId: String?, + @DoNotStrip + @Keep + val notificationUrl: String? ) { /* primary constructor */ @@ -28,11 +31,13 @@ data class CastOptions( if (this === other) return true if (other !is CastOptions) return false return Objects.deepEquals(this.receiverApplicationId, other.receiverApplicationId) + && Objects.deepEquals(this.notificationUrl, other.notificationUrl) } override fun hashCode(): Int { return arrayOf( - receiverApplicationId + receiverApplicationId, + notificationUrl ).contentDeepHashCode() } @@ -44,8 +49,8 @@ data class CastOptions( @Keep @Suppress("unused") @JvmStatic - private fun fromCpp(receiverApplicationId: String?): CastOptions { - return CastOptions(receiverApplicationId) + private fun fromCpp(receiverApplicationId: String?, notificationUrl: String?): CastOptions { + return CastOptions(receiverApplicationId, notificationUrl) } } } diff --git a/nitrogen/generated/shared/c++/CastOptions.hpp b/nitrogen/generated/shared/c++/CastOptions.hpp index ce87273..5eba117 100644 --- a/nitrogen/generated/shared/c++/CastOptions.hpp +++ b/nitrogen/generated/shared/c++/CastOptions.hpp @@ -41,10 +41,11 @@ namespace margelo::nitro::omni { struct CastOptions final { public: std::optional receiverApplicationId SWIFT_PRIVATE; + std::optional notificationUrl SWIFT_PRIVATE; public: CastOptions() = default; - explicit CastOptions(std::optional receiverApplicationId): receiverApplicationId(receiverApplicationId) {} + explicit CastOptions(std::optional receiverApplicationId, std::optional notificationUrl): receiverApplicationId(receiverApplicationId), notificationUrl(notificationUrl) {} public: friend bool operator==(const CastOptions& lhs, const CastOptions& rhs) = default; @@ -60,12 +61,14 @@ namespace margelo::nitro { static inline margelo::nitro::omni::CastOptions fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { jsi::Object obj = arg.asObject(runtime); return margelo::nitro::omni::CastOptions( - JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId"))) + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "notificationUrl"))) ); } static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::omni::CastOptions& arg) { jsi::Object obj(runtime); obj.setProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId"), JSIConverter>::toJSI(runtime, arg.receiverApplicationId)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "notificationUrl"), JSIConverter>::toJSI(runtime, arg.notificationUrl)); return obj; } static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { @@ -77,6 +80,7 @@ namespace margelo::nitro { return false; } if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "notificationUrl")))) return false; return true; } }; diff --git a/src/types/source.ts b/src/types/source.ts index 342072e..4716a17 100644 --- a/src/types/source.ts +++ b/src/types/source.ts @@ -16,6 +16,8 @@ export interface Source { export interface CastOptions { receiverApplicationId?: string; + // deep link opened when the cast notification is tapped (android only) + notificationUrl?: string; } export interface VideoSrc {