feat(cast): clicking on the notif opens specified path

This commit is contained in:
2026-08-14 23:42:34 +02:00
parent 0545e06a04
commit 336db26fc1
10 changed files with 82 additions and 15 deletions
+7
View File
@@ -16,5 +16,12 @@
<action android:name="androidx.media3.session.MediaSessionService" /> <action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter> </intent-filter>
</service> </service>
<activity
android:name="dev.zoriya.omni.OmniCastTapActivity"
android:exported="false"
android:excludeFromRecents="true"
android:noHistory="true"
android:taskAffinity=""
android:theme="@android:style/Theme.NoDisplay" />
</application> </application>
</manifest> </manifest>
@@ -26,11 +26,7 @@ class OmniCastOptionsProvider : OptionsProvider {
.setSmallIconDrawableResId( .setSmallIconDrawableResId(
androidx.media3.session.R.drawable.media3_notification_small_icon androidx.media3.session.R.drawable.media3_notification_small_icon
) )
.apply { .setTargetActivityClassName(OmniCastTapActivity::class.java.name)
context.packageManager.getLaunchIntentForPackage(context.packageName)
?.component?.className
?.let { setTargetActivityClassName(it) }
}
.build() .build()
val mediaOptions = CastMediaOptions.Builder() val mediaOptions = CastMediaOptions.Builder()
@@ -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()
}
}
@@ -128,6 +128,12 @@ class OmniPlayer(
val player: Player = runOnMainThreadSync { val player: Player = runOnMainThreadSync {
castOptions?.receiverApplicationId?.let { receiverApplicationId = it } 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 { val cc = try {
CastContext.getSharedInstance(ctx) CastContext.getSharedInstance(ctx)
} catch (_: Throwable) { } catch (_: Throwable) {
@@ -648,6 +654,10 @@ class OmniPlayer(
// Receiver application id passed at runtime via OmniProvider's `cast` // Receiver application id passed at runtime via OmniProvider's `cast`
// prop; read by OmniCastOptionsProvider when the Cast SDK initializes. // prop; read by OmniCastOptionsProvider when the Cast SDK initializes.
var receiverApplicationId: String? = null var receiverApplicationId: String? = null
// `cast.notificationUrl`, read back by OmniCastTapActivity.
const val OMNI_PREFS = "dev.zoriya.omni"
const val NOTIFICATION_URL_PREF = "notificationUrl"
} }
} }
+4 -1
View File
@@ -511,7 +511,10 @@ function App(): React.JSX.Element {
<OmniProvider <OmniProvider
source={hasSource ? source : undefined} source={hasSource ? source : undefined}
backend={{ android: backend }} backend={{ android: backend }}
cast={{ receiverApplicationId: "D8FB0FC1" }} cast={{
receiverApplicationId: "D8FB0FC1",
notificationUrl: "omniexample://remote",
}}
showNotification showNotification
> >
<PlayerExample <PlayerExample
@@ -25,6 +25,12 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="omniexample" />
</intent-filter>
</activity> </activity>
<meta-data <meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
+7 -3
View File
@@ -34,8 +34,11 @@ namespace margelo::nitro::omni {
static const auto clazz = javaClassStatic(); static const auto clazz = javaClassStatic();
static const auto fieldReceiverApplicationId = clazz->getField<jni::JString>("receiverApplicationId"); static const auto fieldReceiverApplicationId = clazz->getField<jni::JString>("receiverApplicationId");
jni::local_ref<jni::JString> receiverApplicationId = this->getFieldValue(fieldReceiverApplicationId); jni::local_ref<jni::JString> receiverApplicationId = this->getFieldValue(fieldReceiverApplicationId);
static const auto fieldNotificationUrl = clazz->getField<jni::JString>("notificationUrl");
jni::local_ref<jni::JString> notificationUrl = this->getFieldValue(fieldNotificationUrl);
return CastOptions( 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]] [[maybe_unused]]
static jni::local_ref<JCastOptions::javaobject> fromCpp(const CastOptions& value) { static jni::local_ref<JCastOptions::javaobject> fromCpp(const CastOptions& value) {
using JSignature = JCastOptions(jni::alias_ref<jni::JString>); using JSignature = JCastOptions(jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>);
static const auto clazz = javaClassStatic(); static const auto clazz = javaClassStatic();
static const auto create = clazz->getStaticMethod<JSignature>("fromCpp"); static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
return create( return create(
clazz, 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
); );
} }
}; };
@@ -20,7 +20,10 @@ import java.util.Objects
data class CastOptions( data class CastOptions(
@DoNotStrip @DoNotStrip
@Keep @Keep
val receiverApplicationId: String? val receiverApplicationId: String?,
@DoNotStrip
@Keep
val notificationUrl: String?
) { ) {
/* primary constructor */ /* primary constructor */
@@ -28,11 +31,13 @@ data class CastOptions(
if (this === other) return true if (this === other) return true
if (other !is CastOptions) return false if (other !is CastOptions) return false
return Objects.deepEquals(this.receiverApplicationId, other.receiverApplicationId) return Objects.deepEquals(this.receiverApplicationId, other.receiverApplicationId)
&& Objects.deepEquals(this.notificationUrl, other.notificationUrl)
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return arrayOf<Any?>( return arrayOf<Any?>(
receiverApplicationId receiverApplicationId,
notificationUrl
).contentDeepHashCode() ).contentDeepHashCode()
} }
@@ -44,8 +49,8 @@ data class CastOptions(
@Keep @Keep
@Suppress("unused") @Suppress("unused")
@JvmStatic @JvmStatic
private fun fromCpp(receiverApplicationId: String?): CastOptions { private fun fromCpp(receiverApplicationId: String?, notificationUrl: String?): CastOptions {
return CastOptions(receiverApplicationId) return CastOptions(receiverApplicationId, notificationUrl)
} }
} }
} }
+6 -2
View File
@@ -41,10 +41,11 @@ namespace margelo::nitro::omni {
struct CastOptions final { struct CastOptions final {
public: public:
std::optional<std::string> receiverApplicationId SWIFT_PRIVATE; std::optional<std::string> receiverApplicationId SWIFT_PRIVATE;
std::optional<std::string> notificationUrl SWIFT_PRIVATE;
public: public:
CastOptions() = default; CastOptions() = default;
explicit CastOptions(std::optional<std::string> receiverApplicationId): receiverApplicationId(receiverApplicationId) {} explicit CastOptions(std::optional<std::string> receiverApplicationId, std::optional<std::string> notificationUrl): receiverApplicationId(receiverApplicationId), notificationUrl(notificationUrl) {}
public: public:
friend bool operator==(const CastOptions& lhs, const CastOptions& rhs) = default; 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) { static inline margelo::nitro::omni::CastOptions fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime); jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::omni::CastOptions( return margelo::nitro::omni::CastOptions(
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId"))) JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "notificationUrl")))
); );
} }
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::omni::CastOptions& arg) { static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::omni::CastOptions& arg) {
jsi::Object obj(runtime); jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.receiverApplicationId)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.receiverApplicationId));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "notificationUrl"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.notificationUrl));
return obj; return obj;
} }
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
@@ -77,6 +80,7 @@ namespace margelo::nitro {
return false; return false;
} }
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId")))) return false; if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "notificationUrl")))) return false;
return true; return true;
} }
}; };
+2
View File
@@ -16,6 +16,8 @@ export interface Source {
export interface CastOptions { export interface CastOptions {
receiverApplicationId?: string; receiverApplicationId?: string;
// deep link opened when the cast notification is tapped (android only)
notificationUrl?: string;
} }
export interface VideoSrc { export interface VideoSrc {