Add Supa account and search changes

This commit is contained in:
Fabian Freund
2026-05-22 18:10:22 +02:00
parent 3a19865b2e
commit 51289f1266
374 changed files with 54061 additions and 5013 deletions
@@ -32,10 +32,11 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.PluginRegistry
import eu.weblibre.simple_intent_receiver.pigeons.IntentHost
import eu.weblibre.simple_intent_receiver.pigeons.Intent as PigeonIntent
import eu.weblibre.simple_intent_receiver.pigeons.IntentGatekeeperHostApi
class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.NewIntentListener {
class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.NewIntentListener, IntentHost {
companion object {
// Stable names that must match the notification replay path and shared-prefs schema.
private const val PREFS_NAME = "weblibre_intent_gatekeeper"
@@ -53,10 +54,28 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
private var activity: Activity? = null
private var binaryMessenger: io.flutter.plugin.common.BinaryMessenger? = null
/**
* Caches the launch intent so Dart can retrieve it after setUp().
* On cold start, onAttachedToActivity fires before Dart registers its
* Pigeon handler, so the initial sendIntent message is lost. This field
* lets Dart call getInitialIntent() to recover it.
*
* On Android configuration change (rotation, theme switch) the activity
* is recreated and onAttachedToActivity fires again with the same
* launching intent. The `lastHandledIntent` guard below prevents
* re-caching that identical intent. If a NEW deep link arrives via the
* launcher between two Dart-side reads of getInitialIntent(),
* pendingInitialIntent is overwritten — only the latest intent is
* delivered. This is intentional: dropping the stale one keeps the
* "initial" intent meaning "what should the app open into right now".
*/
private var pendingInitialIntent: PigeonIntent? = null
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
intentReceiver = IntentReceiver(flutterPluginBinding.binaryMessenger)
binaryMessenger = flutterPluginBinding.binaryMessenger
IntentHost.setUp(flutterPluginBinding.binaryMessenger, this)
IntentGatekeeperHostApi.setUp(
flutterPluginBinding.binaryMessenger,
IntentGatekeeperHostApiImpl(flutterPluginBinding.applicationContext),
@@ -65,10 +84,19 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
intentReceiver = null
binaryMessenger?.let { IntentGatekeeperHostApi.setUp(it, null) }
binaryMessenger?.let {
IntentHost.setUp(it, null)
IntentGatekeeperHostApi.setUp(it, null)
}
binaryMessenger = null
}
override fun getInitialIntent(): PigeonIntent? {
val intent = pendingInitialIntent
pendingInitialIntent = null
return intent
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
binding.addOnNewIntentListener(this)
@@ -76,7 +104,10 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
binding.activity.intent?.let { intent ->
val uri = intent.toUri(0)
if (lastHandledIntent != uri) {
handleIntent(intent)
// Cache the launch intent for Dart to retrieve via getInitialIntent().
// Don't send via Pigeon here — the Dart handler isn't registered yet
// during cold start so the message would be lost.
pendingInitialIntent = prepareIntentForDelivery(intent)
lastHandledIntent = uri
}
}
@@ -100,8 +131,7 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
return handleIntent(intent)
}
private fun handleIntent(intent: Intent): Boolean {
// Grant URI permissions for content URIs
private fun grantUriPermissions(intent: Intent) {
intent.data?.let { uri ->
if (uri.scheme == "content") {
try {
@@ -116,7 +146,6 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
}
}
// Handle SEND action with STREAM extra
intent.getStringExtra(Intent.EXTRA_STREAM)?.let { streamUri ->
try {
val uri = Uri.parse(streamUri)
@@ -131,15 +160,23 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
Log.w("SimpleIntentReceiver", "Could not grant URI permission for stream: $streamUri", e)
}
}
}
private fun handleIntent(intent: Intent): Boolean {
val pigeonIntent = prepareIntentForDelivery(intent)
intentReceiver?.sendIntent(pigeonIntent)
return true
}
private fun prepareIntentForDelivery(intent: Intent): PigeonIntent {
grantUriPermissions(intent)
if (intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0) {
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val notificationApproval = consumeNotificationApproval(intent)
val pigeonIntent = convertToPigeonIntent(intent, notificationApproval)
intentReceiver?.sendIntent(pigeonIntent)
return true
return convertToPigeonIntent(intent, notificationApproval)
}
private fun resolveCallerPackage(intent: Intent, notificationApproval: NotificationApproval?): String? {
@@ -270,6 +270,43 @@ private open class IntentPigeonCodec : StandardMessageCodec() {
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface IntentHost {
/**
* Returns the launch intent that started the activity, if any.
* This allows Dart to retrieve an intent that arrived before
* IntentEvents.setUp() was called (cold-start deep links).
* Returns null if no launch intent is pending.
*/
fun getInitialIntent(): Intent?
companion object {
/** The codec used by IntentHost. */
val codec: MessageCodec<Any?> by lazy {
IntentPigeonCodec()
}
/** Sets up an instance of `IntentHost` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: IntentHost?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.simple_intent_receiver.IntentHost.getInitialIntent$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.getInitialIntent())
} catch (exception: Throwable) {
IntentPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
class IntentEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
@@ -26,11 +26,61 @@ class IntentReceiver extends IntentEvents {
final _controller = StreamController<Intent>.broadcast();
int? _lastAdded;
Stream<Intent> get events => _controller.stream;
/// Intent events, including the cold-start launch intent for each listener.
///
/// New-intent callbacks still arrive through the broadcast controller below.
/// The initial intent is replayed per listener so existing callers that only
/// listen to [events] continue to receive terminated-app launches.
Stream<Intent> get events {
return Stream.multi((controller) {
final subscription = _controller.stream.listen(
controller.add,
onError: controller.addError,
onDone: controller.close,
);
unawaited(
initialIntent.then(
(intent) {
if (intent != null && !controller.isClosed) {
controller.add(intent);
}
},
onError: (Object error, StackTrace stackTrace) {
if (!controller.isClosed) {
controller.addError(error, stackTrace);
}
},
),
);
controller.onCancel = subscription.cancel;
}, isBroadcast: true);
}
/// The launch intent recovered from the host, if any. Resolves to
/// `Future<null>` for instances not constructed via [IntentReceiver.setUp]
/// (e.g. test fakes / subclasses), so callers can always `await` without
/// guarding for `LateInitializationError`.
///
/// On cold start the Android plugin sees the launch intent from
/// onAttachedToActivity before Dart has registered the Pigeon handler, so it
/// caches the value for Dart to recover. The [events] stream already replays
/// this value for compatibility; use this future directly only when the
/// launch intent needs one-shot handling outside the event stream.
///
/// Note on rotation: the Android plugin's `pendingInitialIntent` cache
/// is intentionally overwritten on configuration change. If the user
/// triggers a new deep link via the activity launcher before Dart
/// drains the previous initial intent (rare — the future is read on
/// IntentReceiver construction, which happens during app bootstrap),
/// only the newest intent is delivered.
Future<Intent?> initialIntent = Future.value(null);
@override
void onIntentReceived(int sequence, Intent intent) {
if (_lastAdded == null || sequence > _lastAdded!) {
_lastAdded = sequence;
_controller.add(intent);
}
}
@@ -44,6 +94,12 @@ class IntentReceiver extends IntentEvents {
binaryMessenger: binaryMessenger,
messageChannelSuffix: messageChannelSuffix,
);
final host = IntentHost(
binaryMessenger: binaryMessenger,
messageChannelSuffix: messageChannelSuffix,
);
initialIntent = host.getInitialIntent();
}
Future<void> dispose() async {
@@ -199,6 +199,43 @@ class _PigeonCodec extends StandardMessageCodec {
}
}
class IntentHost {
/// Constructor for [IntentHost]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
IntentHost({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
/// Returns the launch intent that started the activity, if any.
/// This allows Dart to retrieve an intent that arrived before
/// IntentEvents.setUp() was called (cold-start deep links).
/// Returns null if no launch intent is pending.
Future<Intent?> getInitialIntent() async {
final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentHost.getInitialIntent$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: true,
)
;
return pigeonVar_replyValue as Intent?;
}
}
abstract class IntentEvents {
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -49,6 +49,15 @@ class Intent {
dartPackageName: 'simple_intent_receiver',
),
)
@HostApi()
abstract class IntentHost {
/// Returns the launch intent that started the activity, if any.
/// This allows Dart to retrieve an intent that arrived before
/// IntentEvents.setUp() was called (cold-start deep links).
/// Returns null if no launch intent is pending.
Intent? getInitialIntent();
}
@FlutterApi()
abstract class IntentEvents {
void onIntentReceived(int sequence, Intent intent);