From 5b8d2c95c2beddb8aa7e003d68f3fe99a031449c Mon Sep 17 00:00:00 2001 From: chan9an Date: Sun, 1 Mar 2026 13:44:51 +0530 Subject: [PATCH 1/3] refactor: simplify Android widget deep linking by directly launching MainActivity --- .../TaskWarriorWidgetProvider.kt | 381 +++++++++--------- .../home/controllers/home_controller.dart | 23 +- .../splash/controllers/splash_controller.dart | 27 +- lib/app/services/deep_link_service.dart | 31 +- lib/main.dart | 17 +- 5 files changed, 253 insertions(+), 226 deletions(-) diff --git a/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt b/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt index d57d4cb0..157413c8 100644 --- a/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt +++ b/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt @@ -1,239 +1,228 @@ package com.ccextractor.taskwarriorflutter + import android.annotation.TargetApi +import android.app.PendingIntent import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider import android.content.Context +import android.content.Intent import android.net.Uri +import android.os.Build import android.widget.RemoteViews -import es.antonborri.home_widget.HomeWidgetBackgroundIntent +import android.widget.RemoteViewsService import es.antonborri.home_widget.HomeWidgetLaunchIntent -import es.antonborri.home_widget.HomeWidgetProvider import es.antonborri.home_widget.HomeWidgetPlugin +import org.json.JSONArray as OrgJSONArray import org.json.JSONException -import android.content.Intent -import android.widget.RemoteViewsService import org.json.JSONObject -import org.json.JSONArray as OrgJSONArray -import android.os.Bundle -import android.app.PendingIntent -import android.appwidget.AppWidgetProvider -import android.os.Build - @TargetApi(Build.VERSION_CODES.CUPCAKE) class TaskWarriorWidgetProvider : AppWidgetProvider() { - override fun onReceive(context: Context, intent: Intent) { - // Handle the custom action from your Widget buttons/list - if (intent.action == "TASK_ACTION") { - val uuid = intent.getStringExtra("uuid") ?: "" - val launchedFor = intent.getStringExtra("launchedFor") - - // 1. Construct the URI exactly as Flutter expects it - // Scheme: taskwarrior:// - // Host: cardclicked OR addclicked - val deepLinkUri = if (launchedFor == "ADD_TASK") { - Uri.parse("taskwarrior://addclicked") - } else { - // For list items, we attach the UUID - Uri.parse("taskwarrior://cardclicked?uuid=$uuid") - } - - // 2. Create the Intent to open MainActivity - val launchIntent = Intent(context, MainActivity::class.java).apply { - action = Intent.ACTION_VIEW - data = deepLinkUri - // These flags ensure the app opens correctly whether running or not - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - } - - context.startActivity(launchIntent) - } - super.onReceive(context, intent) - } - fun getLayoutId(context: Context) : Int{ - val sharedPrefs = HomeWidgetPlugin.getData(context) - val theme = sharedPrefs.getString("themeMode", "") - val layoutId = if (theme.equals("dark")) { - R.layout.taskwarrior_layout_dark // Define a dark mode layout in your resources - } else { - R.layout.taskwarrior_layout - } - return layoutId - } -@TargetApi(Build.VERSION_CODES.DONUT) -override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { - appWidgetIds.forEach { widgetId -> - // 1. Get the latest data from HomeWidget/SharedPrefs + fun getLayoutId(context: Context): Int { val sharedPrefs = HomeWidgetPlugin.getData(context) - val tasks = sharedPrefs.getString("tasks", "") - - // 2. Create the Intent for the ListView service - // We add the widgetId to the data URI to make it unique, preventing caching issues - val intent = Intent(context, ListViewRemoteViewsService::class.java).apply { - putExtra("tasksJsonString", tasks) - data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME) + widgetId) - } - - // 3. Initialize RemoteViews with the THEMED layout (getLayoutId handles dark/light logic) - val views = RemoteViews(context.packageName, getLayoutId(context)).apply { - - // Set up the Logo click (Open App) - val pendingIntent: PendingIntent = HomeWidgetLaunchIntent.getActivity( - context, - MainActivity::class.java - ) - setOnClickPendingIntent(R.id.logo, pendingIntent) - - // Set up the Add Button click (Custom Action) - val intent_for_add = Intent(context, TaskWarriorWidgetProvider::class.java).apply { - action = "TASK_ACTION" - putExtra("launchedFor", "ADD_TASK") - // Unique data to ensure the broadcast is fresh - data = Uri.parse("taskwarrior://addtask/$widgetId") - } - - val pendingIntentAdd: PendingIntent = PendingIntent.getBroadcast( - context, - widgetId, - intent_for_add, - PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) - setOnClickPendingIntent(R.id.add_btn, pendingIntentAdd) - - // Attach the adapter to the ListView - setRemoteAdapter(R.id.list_view, intent) - } - - // 4. Set up the Click Template for List Items (Deep Linking) - val clickPendingIntent: PendingIntent = Intent( - context, - TaskWarriorWidgetProvider::class.java - ).run { - action = "TASK_ACTION" - // Important: Use widgetId as requestCode to keep it unique - PendingIntent.getBroadcast( - context, - widgetId, - this, - PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) + val theme = sharedPrefs.getString("themeMode", "") + val layoutId = + if (theme.equals("dark")) { + R.layout.taskwarrior_layout_dark // Define a dark mode layout in your resources + } else { + R.layout.taskwarrior_layout + } + return layoutId + } + @TargetApi(Build.VERSION_CODES.DONUT) + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + appWidgetIds.forEach { widgetId -> + // 1. Get the latest data from HomeWidget/SharedPrefs + val sharedPrefs = HomeWidgetPlugin.getData(context) + val tasks = sharedPrefs.getString("tasks", "") + + // 2. Create the Intent for the ListView service + // We add the widgetId to the data URI to make it unique, preventing caching issues + val intent = + Intent(context, ListViewRemoteViewsService::class.java).apply { + putExtra("tasksJsonString", tasks) + data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME) + widgetId) + } + + // 3. Initialize RemoteViews with the THEMED layout (getLayoutId handles dark/light + // logic) + val views = + RemoteViews(context.packageName, getLayoutId(context)).apply { + + // Set up the Logo click (Open App) + val pendingIntent: PendingIntent = + HomeWidgetLaunchIntent.getActivity( + context, + MainActivity::class.java + ) + setOnClickPendingIntent(R.id.logo, pendingIntent) + + // Set up the Add Button click (Direct to MainActivity) + val intentForAdd = + Intent(context, MainActivity::class.java).apply { + action = Intent.ACTION_VIEW + data = Uri.parse("taskwarrior://addclicked") + flags = + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP + } + + val pendingIntentAdd: PendingIntent = + PendingIntent.getActivity( + context, + widgetId, + intentForAdd, + PendingIntent.FLAG_IMMUTABLE or + PendingIntent.FLAG_UPDATE_CURRENT + ) + setOnClickPendingIntent(R.id.add_btn, pendingIntentAdd) + + // Attach the adapter to the ListView + setRemoteAdapter(R.id.list_view, intent) + } + + // 4. Set up the Click Template for List Items (Deep Linking) + val clickIntentTemplate = + Intent(context, MainActivity::class.java).apply { + action = Intent.ACTION_VIEW + flags = + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP + } + val clickPendingIntentTemplate: PendingIntent = + PendingIntent.getActivity( + context, + widgetId, + clickIntentTemplate, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + views.setPendingIntentTemplate(R.id.list_view, clickPendingIntentTemplate) + + // 5. THE THEME FIX: Notify the manager that the list data/layout needs a refresh + appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, R.id.list_view) + + // 6. Push the update to the widget + appWidgetManager.updateAppWidget(widgetId, views) } - views.setPendingIntentTemplate(R.id.list_view, clickPendingIntent) - - // 5. THE THEME FIX: Notify the manager that the list data/layout needs a refresh - appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, R.id.list_view) - - // 6. Push the update to the widget - appWidgetManager.updateAppWidget(widgetId, views) + super.onUpdate(context, appWidgetManager, appWidgetIds) } - super.onUpdate(context, appWidgetManager, appWidgetIds) -} } +} + class ListViewRemoteViewsFactory( - private val context: Context, - private val tasksJsonString: String? + private val context: Context, + private val tasksJsonString: String? ) : RemoteViewsService.RemoteViewsFactory { private val tasks = mutableListOf() override fun onCreate() {} - override fun onDataSetChanged() { - tasks.clear() // Add this! - val sharedPrefs = HomeWidgetPlugin.getData(context) - val latestTasksJson = sharedPrefs.getString("tasks", "") - - if (!latestTasksJson.isNullOrEmpty()) { - try { - val jsonArray = OrgJSONArray(latestTasksJson) - for (i in 0 until jsonArray.length()) { - tasks.add(Task.fromJson(jsonArray.getJSONObject(i))) - } - } catch (e: JSONException) { - e.printStackTrace() - } - } - } + override fun onDataSetChanged() { + tasks.clear() // Add this! + val sharedPrefs = HomeWidgetPlugin.getData(context) + val latestTasksJson = sharedPrefs.getString("tasks", "") + + if (!latestTasksJson.isNullOrEmpty()) { + try { + val jsonArray = OrgJSONArray(latestTasksJson) + for (i in 0 until jsonArray.length()) { + tasks.add(Task.fromJson(jsonArray.getJSONObject(i))) + } + } catch (e: JSONException) { + e.printStackTrace() + } + } + } override fun onDestroy() {} override fun getCount(): Int = tasks.size - fun getListItemLayoutId(): Int{ - val sharedPrefs = HomeWidgetPlugin.getData(context) - val theme = sharedPrefs.getString("themeMode", "") - val layoutId = if (theme.equals("dark")) { - R.layout.listitem_layout_dark // Define a dark mode layout in your resources - } else { - R.layout.listitem_layout - } - return layoutId - } - fun getListItemLayoutIdForR1(): Int{ - val sharedPrefs = HomeWidgetPlugin.getData(context) - val theme = sharedPrefs.getString("themeMode", "") - val layoutId = if (theme.equals("dark")) { - R.layout.no_tasks_found_li_dark // Define a dark mode layout in your resources - } else { - R.layout.no_tasks_found_li - } - return layoutId - } - fun getDotIdByPriority(p: String) : Int{ - println("PRIORITY: "+p) - if(p.equals("L")) return R.drawable.low_priority_dot - if(p.equals("M")) return R.drawable.mid_priority_dot - if(p.equals("H")) return R.drawable.high_priority_dot - return R.drawable.no_priority_dot - } + fun getListItemLayoutId(): Int { + val sharedPrefs = HomeWidgetPlugin.getData(context) + val theme = sharedPrefs.getString("themeMode", "") + val layoutId = + if (theme.equals("dark")) { + R.layout.listitem_layout_dark // Define a dark mode layout in your resources + } else { + R.layout.listitem_layout + } + return layoutId + } + fun getListItemLayoutIdForR1(): Int { + val sharedPrefs = HomeWidgetPlugin.getData(context) + val theme = sharedPrefs.getString("themeMode", "") + val layoutId = + if (theme.equals("dark")) { + R.layout.no_tasks_found_li_dark // Define a dark mode layout in your resources + } else { + R.layout.no_tasks_found_li + } + return layoutId + } + fun getDotIdByPriority(p: String): Int { + println("PRIORITY: " + p) + if (p.equals("L")) return R.drawable.low_priority_dot + if (p.equals("M")) return R.drawable.mid_priority_dot + if (p.equals("H")) return R.drawable.high_priority_dot + return R.drawable.no_priority_dot + } override fun getViewAt(position: Int): RemoteViews { val task = tasks[position] - if(task.uuid.equals("NO_TASK")) - return RemoteViews(context.packageName, getListItemLayoutIdForR1()).apply { - if(task.priority.equals("1")) - setTextViewText(R.id.tv, "No tasks added yet") - if(task.priority.equals("2")) - setTextViewText(R.id.tv, "Filters applied are hiding all tasks") - } - return RemoteViews(context.packageName, getListItemLayoutId()).apply { - setTextViewText(R.id.todo__title, task.title) - setImageViewResource(R.id.dot, getDotIdByPriority(task.priority)) - val a = Intent().apply { - - Bundle().also { extras -> - extras.putString("action", "show_task") - extras.putString("uuid", tasks[position].uuid) - putExtras(extras) - } - - } - setOnClickFillInIntent(R.id.list_item_container,a) - } - + if (task.uuid.equals("NO_TASK")) + return RemoteViews(context.packageName, getListItemLayoutIdForR1()).apply { + if (task.priority.equals("1")) setTextViewText(R.id.tv, "No tasks added yet") + if (task.priority.equals("2")) + setTextViewText(R.id.tv, "Filters applied are hiding all tasks") + } + return RemoteViews(context.packageName, getListItemLayoutId()).apply { + setTextViewText(R.id.todo__title, task.title) + setImageViewResource(R.id.dot, getDotIdByPriority(task.priority)) + val fillInIntent = + Intent().apply { + data = Uri.parse("taskwarrior://cardclicked?uuid=${task.uuid}") + } + setOnClickFillInIntent(R.id.list_item_container, fillInIntent) + } } override fun getLoadingView(): RemoteViews? = null - override fun getViewTypeCount(): Int = 2 + override fun getViewTypeCount(): Int = 2 override fun getItemId(position: Int): Long = position.toLong() override fun hasStableIds(): Boolean = true } + class ListViewRemoteViewsService : RemoteViewsService() { - override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { - val tasksJsonString = intent.getStringExtra("tasksJsonString") - return ListViewRemoteViewsFactory(applicationContext, tasksJsonString) - } + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { + val tasksJsonString = intent.getStringExtra("tasksJsonString") + return ListViewRemoteViewsFactory(applicationContext, tasksJsonString) + } +} + +data class Task( + val title: String, + val urgencyLevel: String, + val uuid: String, + val priority: String +) { + companion object { + fun fromJson(json: JSONObject): Task { + val title = json.optString("description", "") + val urgencyLevel = json.optString("urgency", "") + val uuid = json.optString("uuid", "") + val priority = json.optString("priority", "") + return Task(title, urgencyLevel, uuid, priority) + } + } } -data class Task(val title: String, val urgencyLevel: String,val uuid:String, val priority: String) { - companion object { - fun fromJson(json: JSONObject): Task { - val title = json.optString("description", "") - val urgencyLevel = json.optString("urgency", "") - val uuid = json.optString("uuid","") - val priority = json.optString("priority", "") - return Task(title, urgencyLevel, uuid, priority) - } - } -} \ No newline at end of file diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index 053f2bcc..e0723672 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -12,8 +12,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:taskwarrior/app/models/filters.dart'; import 'package:taskwarrior/app/models/json/task.dart'; -import 'package:taskwarrior/app/models/storage.dart'; import 'package:taskwarrior/app/models/storage/client.dart'; +import 'package:taskwarrior/app/models/storage.dart'; import 'package:taskwarrior/app/models/tag_meta_data.dart'; import 'package:taskwarrior/app/modules/home/controllers/widget.controller.dart'; import 'package:taskwarrior/app/modules/splash/controllers/splash_controller.dart'; @@ -70,6 +70,7 @@ class HomeController extends GetxController { @override void onInit() { + debugPrint("🚀 BOOT: HomeController.onInit()"); super.onInit(); storage = Storage( Directory( @@ -78,6 +79,7 @@ class HomeController extends GetxController { ); serverCertExists = RxBool(storage.guiPemFiles.serverCertExists()); addListenerToScrollController(); + _profileSet(); loadDelayTask(); initLanguageAndDarkMode(); @@ -127,9 +129,19 @@ class HomeController extends GetxController { @override void onReady() { super.onReady(); - if (Get.isRegistered()) { - Get.find().consumePendingActions(this); - } + // Automatically check for any queued Deep Links when Home spins up. + // We delay slightly to ensure the Navigator route swap finishes first, avoiding widget tree lock. + Future.delayed(const Duration(milliseconds: 50), () { + if (isClosed) return; + if (Get.isRegistered()) { + final deepLinkService = Get.find(); + if (deepLinkService.queuedUri != null) { + debugPrint( + "TRACE: HomeController.onReady() consuming deferred queue!"); + deepLinkService.consumePendingActions(this); + } + } + }); } Future> getUniqueProjects() async { @@ -577,8 +589,7 @@ class HomeController extends GetxController { await synchronize(context, false); } if (context.mounted) { - final tColors = - Theme.of(context).extension()!; + final tColors = Theme.of(context).extension()!; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( diff --git a/lib/app/modules/splash/controllers/splash_controller.dart b/lib/app/modules/splash/controllers/splash_controller.dart index 126aaebb..b1e3742d 100644 --- a/lib/app/modules/splash/controllers/splash_controller.dart +++ b/lib/app/modules/splash/controllers/splash_controller.dart @@ -13,6 +13,7 @@ import 'package:taskwarrior/app/routes/app_pages.dart'; import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart'; import 'package:taskwarrior/app/utils/taskfunctions/profiles.dart'; import 'package:taskwarrior/app/v3/models/task.dart'; +import 'package:taskwarrior/app/services/deep_link_service.dart'; class SplashController extends GetxController { late Rx baseDirectory = Directory('').obs; @@ -23,14 +24,26 @@ class SplashController extends GetxController { @override void onInit() async { + debugPrint("🚀 BOOT: SplashController.onInit()"); super.onInit(); + + // If we don't, HomeController will boot blind and crash trying to read empty paths. + await initBaseDir(); + _checkProfiles(); + profilesMap.value = _profiles.profilesMap(); + currentProfile.value = _profiles.getCurrentProfile()!; + + // FIX 2: NOW we check if we should bypass the slow UI stuff. + final deepLinkService = Get.find(); + if (deepLinkService.queuedUri != null) { + debugPrint("🚀 TRACE: Bypassing Splash routing for queued URI"); + Get.offNamed(Routes.HOME); + return; // Skip the slow app updates and onboarding checks + } + + // Normal boot sequence for people just opening the app normally await checkForUpdate(); - initBaseDir().then((_) { - _checkProfiles(); - profilesMap.value = _profiles.profilesMap(); - currentProfile.value = _profiles.getCurrentProfile()!; - sendToNextPage(); - }); + sendToNextPage(); } Future initBaseDir() async { @@ -160,4 +173,4 @@ class SplashController extends GetxController { debugPrint(e.toString()); } } -} +} \ No newline at end of file diff --git a/lib/app/services/deep_link_service.dart b/lib/app/services/deep_link_service.dart index f016c93c..5d72fa13 100644 --- a/lib/app/services/deep_link_service.dart +++ b/lib/app/services/deep_link_service.dart @@ -7,16 +7,21 @@ import 'package:taskwarrior/app/routes/app_pages.dart'; class DeepLinkService extends GetxService { late AppLinks _appLinks; - Uri? _queuedUri; + String? queuedUri; - @override - void onReady() { - super.onReady(); - _initDeepLinks(); - } - - void _initDeepLinks() { + Future init() async { _appLinks = AppLinks(); + + try { + final initialUri = await _appLinks.getInitialLink(); + if (initialUri != null) { + queuedUri = initialUri.toString(); + debugPrint('🔗 INITIAL LINK QUEUED: $queuedUri'); + } + } catch (e) { + debugPrint('Deep link init error (safe to ignore on unsupported platforms): $e'); + } + _appLinks.uriLinkStream.listen((uri) { debugPrint('🔗 LINK RECEIVED: $uri'); _handleWidgetUri(uri); @@ -28,15 +33,15 @@ class DeepLinkService extends GetxService { _executeAction(uri, Get.find()); } else { debugPrint("⏳ HomeController not ready. Queuing action."); - _queuedUri = uri; + queuedUri = uri.toString(); } } void consumePendingActions(HomeController controller) { - if (_queuedUri != null) { + if (queuedUri != null) { debugPrint("🚀 Executing queued action..."); - _executeAction(_queuedUri!, controller); - _queuedUri = null; + _executeAction(Uri.parse(queuedUri!), controller); + queuedUri = null; } } @@ -66,4 +71,4 @@ class DeepLinkService extends GetxService { } } } -} +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 09f0268c..9f00e179 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,8 +3,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -// 1. Add this import -import 'package:app_links/app_links.dart'; import 'package:taskwarrior/app/services/deep_link_service.dart'; import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; @@ -29,6 +27,9 @@ DynamicLibrary loadNativeLibrary() { } void main() async { + debugPrint("🚀 BOOT: main() started"); + WidgetsFlutterBinding.ensureInitialized(); + debugPrint = (String? message, {int? wrapWidth}) { if (message != null) { debugPrintSynchronously(message, wrapWidth: wrapWidth); @@ -39,16 +40,24 @@ void main() async { loadNativeLibrary(); await RustLib.init(); - WidgetsFlutterBinding.ensureInitialized(); await AppSettings.init(); - Get.put(DeepLinkService(), permanent: true); + // fix: Actually await the service initialization so the OS intent is caught BEFORE runApp. + await Get.putAsync(() async { + final service = DeepLinkService(); + await service.init(); + return service; + }, permanent: true); runApp( GetMaterialApp( darkTheme: darkTheme, theme: lightTheme, title: "Application", initialRoute: AppPages.INITIAL, + unknownRoute: AppPages.routes.firstWhere( + (page) => page.name == AppPages.INITIAL, + orElse: () => AppPages.routes.first, + ), getPages: AppPages.routes, themeMode: AppSettings.isDarkMode ? ThemeMode.dark : ThemeMode.light, ), From 76d98de4d2d180eb8d050b7ad39c1f63ead38849 Mon Sep 17 00:00:00 2001 From: chan9an Date: Sun, 1 Mar 2026 15:07:48 +0530 Subject: [PATCH 2/3] refactor: address CodeRabbitAI review feedback for lifecycle, bounds checking, and encapsulation --- .../TaskWarriorWidgetProvider.kt | 19 +++++++--- .../home/controllers/home_controller.dart | 18 +++++----- .../splash/controllers/splash_controller.dart | 14 ++++---- lib/app/services/deep_link_service.dart | 35 +++++++++++++------ lib/main.dart | 11 ++++-- 5 files changed, 63 insertions(+), 34 deletions(-) diff --git a/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt b/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt index 157413c8..5eeb70ec 100644 --- a/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt +++ b/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt @@ -122,10 +122,10 @@ class ListViewRemoteViewsFactory( private val tasks = mutableListOf() - override fun onCreate() {} + override fun onCreate() = Unit override fun onDataSetChanged() { - tasks.clear() // Add this! + val newTasks = mutableListOf() val sharedPrefs = HomeWidgetPlugin.getData(context) val latestTasksJson = sharedPrefs.getString("tasks", "") @@ -133,15 +133,18 @@ class ListViewRemoteViewsFactory( try { val jsonArray = OrgJSONArray(latestTasksJson) for (i in 0 until jsonArray.length()) { - tasks.add(Task.fromJson(jsonArray.getJSONObject(i))) + newTasks.add(Task.fromJson(jsonArray.getJSONObject(i))) } } catch (e: JSONException) { e.printStackTrace() } } + // Atomic swap + tasks.clear() + tasks.addAll(newTasks) } - override fun onDestroy() {} + override fun onDestroy() = Unit override fun getCount(): Int = tasks.size @@ -168,7 +171,6 @@ class ListViewRemoteViewsFactory( return layoutId } fun getDotIdByPriority(p: String): Int { - println("PRIORITY: " + p) if (p.equals("L")) return R.drawable.low_priority_dot if (p.equals("M")) return R.drawable.mid_priority_dot if (p.equals("H")) return R.drawable.high_priority_dot @@ -176,6 +178,13 @@ class ListViewRemoteViewsFactory( } override fun getViewAt(position: Int): RemoteViews { + // Safe guard against Android out-of-bounds scrolling crashes + if (position !in tasks.indices) { + return RemoteViews(context.packageName, getListItemLayoutIdForR1()).apply { + setTextViewText(R.id.tv, "Loading...") + } + } + val task = tasks[position] if (task.uuid.equals("NO_TASK")) return RemoteViews(context.packageName, getListItemLayoutIdForR1()).apply { diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index e0723672..3cf342f8 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -129,17 +129,15 @@ class HomeController extends GetxController { @override void onReady() { super.onReady(); - // Automatically check for any queued Deep Links when Home spins up. - // We delay slightly to ensure the Navigator route swap finishes first, avoiding widget tree lock. - Future.delayed(const Duration(milliseconds: 50), () { + // Replaced 50ms delay with a secure PostFrameCallback + WidgetsBinding.instance.addPostFrameCallback((_) { if (isClosed) return; - if (Get.isRegistered()) { - final deepLinkService = Get.find(); - if (deepLinkService.queuedUri != null) { - debugPrint( - "TRACE: HomeController.onReady() consuming deferred queue!"); - deepLinkService.consumePendingActions(this); - } + + final deepLinkService = Get.find(); + if (deepLinkService.queuedUri != null) { + debugPrint( + "🚀 TRACE: HomeController.onReady() consuming deferred queue!"); + deepLinkService.consumePendingActions(this); } }); } diff --git a/lib/app/modules/splash/controllers/splash_controller.dart b/lib/app/modules/splash/controllers/splash_controller.dart index b1e3742d..a126ef0e 100644 --- a/lib/app/modules/splash/controllers/splash_controller.dart +++ b/lib/app/modules/splash/controllers/splash_controller.dart @@ -23,25 +23,27 @@ class SplashController extends GetxController { Profiles get _profiles => Profiles(baseDirectory.value); @override - void onInit() async { + void onInit() { debugPrint("🚀 BOOT: SplashController.onInit()"); super.onInit(); + } + + @override + void onReady() async { + super.onReady(); - // If we don't, HomeController will boot blind and crash trying to read empty paths. await initBaseDir(); _checkProfiles(); profilesMap.value = _profiles.profilesMap(); currentProfile.value = _profiles.getCurrentProfile()!; - // FIX 2: NOW we check if we should bypass the slow UI stuff. final deepLinkService = Get.find(); if (deepLinkService.queuedUri != null) { debugPrint("🚀 TRACE: Bypassing Splash routing for queued URI"); Get.offNamed(Routes.HOME); - return; // Skip the slow app updates and onboarding checks + return; } - // Normal boot sequence for people just opening the app normally await checkForUpdate(); sendToNextPage(); } @@ -173,4 +175,4 @@ class SplashController extends GetxController { debugPrint(e.toString()); } } -} \ No newline at end of file +} diff --git a/lib/app/services/deep_link_service.dart b/lib/app/services/deep_link_service.dart index 5d72fa13..5db40922 100644 --- a/lib/app/services/deep_link_service.dart +++ b/lib/app/services/deep_link_service.dart @@ -1,3 +1,4 @@ +import 'dart:async'; // Add this import at the top import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:app_links/app_links.dart'; @@ -7,7 +8,9 @@ import 'package:taskwarrior/app/routes/app_pages.dart'; class DeepLinkService extends GetxService { late AppLinks _appLinks; - String? queuedUri; + String? _queuedUri; // Made private + String? get queuedUri => _queuedUri; // Added getter + StreamSubscription? _linkSubscription; // Added stream subscription Future init() async { _appLinks = AppLinks(); @@ -15,33 +18,45 @@ class DeepLinkService extends GetxService { try { final initialUri = await _appLinks.getInitialLink(); if (initialUri != null) { - queuedUri = initialUri.toString(); - debugPrint('🔗 INITIAL LINK QUEUED: $queuedUri'); + _queuedUri = initialUri.toString(); + debugPrint('🔗 INITIAL LINK QUEUED: $_queuedUri'); } } catch (e) { - debugPrint('Deep link init error (safe to ignore on unsupported platforms): $e'); + debugPrint('Deep link init error: $e'); } - _appLinks.uriLinkStream.listen((uri) { + _linkSubscription = _appLinks.uriLinkStream.listen((uri) { debugPrint('🔗 LINK RECEIVED: $uri'); _handleWidgetUri(uri); + }, onError: (err) { + debugPrint('🔗 LINK STREAM ERROR: $err'); }); } + @override + void onClose() { + _linkSubscription?.cancel(); + super.onClose(); + } + void _handleWidgetUri(Uri uri) { if (Get.isRegistered()) { _executeAction(uri, Get.find()); } else { debugPrint("⏳ HomeController not ready. Queuing action."); - queuedUri = uri.toString(); + _queuedUri = uri.toString(); } } void consumePendingActions(HomeController controller) { - if (queuedUri != null) { + if (_queuedUri != null) { debugPrint("🚀 Executing queued action..."); - _executeAction(Uri.parse(queuedUri!), controller); - queuedUri = null; + try { + _executeAction(Uri.parse(_queuedUri!), controller); + } catch (e) { + debugPrint("🔗 FAILED TO PARSE URI: $_queuedUri - Error: $e"); + } + _queuedUri = null; } } @@ -71,4 +86,4 @@ class DeepLinkService extends GetxService { } } } -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index 9f00e179..16812cf3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,9 +27,9 @@ DynamicLibrary loadNativeLibrary() { } void main() async { - debugPrint("🚀 BOOT: main() started"); WidgetsFlutterBinding.ensureInitialized(); + // Move the logger override ABOVE the first boot print! debugPrint = (String? message, {int? wrapWidth}) { if (message != null) { debugPrintSynchronously(message, wrapWidth: wrapWidth); @@ -37,6 +37,8 @@ void main() async { } }; + debugPrint("🚀 BOOT: main() started"); + loadNativeLibrary(); await RustLib.init(); @@ -45,7 +47,7 @@ void main() async { // fix: Actually await the service initialization so the OS intent is caught BEFORE runApp. await Get.putAsync(() async { final service = DeepLinkService(); - await service.init(); + await service.init(); return service; }, permanent: true); runApp( @@ -56,7 +58,10 @@ void main() async { initialRoute: AppPages.INITIAL, unknownRoute: AppPages.routes.firstWhere( (page) => page.name == AppPages.INITIAL, - orElse: () => AppPages.routes.first, + orElse: () { + debugPrint("⚠️ Unknown route requested, falling back to default"); + return AppPages.routes.first; + }, ), getPages: AppPages.routes, themeMode: AppSettings.isDarkMode ? ThemeMode.dark : ThemeMode.light, From 8b81c0cea28099d6c0fc7c584000274c943a4c88 Mon Sep 17 00:00:00 2001 From: chan9an Date: Tue, 3 Mar 2026 22:18:34 +0530 Subject: [PATCH 3/3] fix:properly configure deep-link routing context --- android/app/src/main/AndroidManifest.xml | 4 ++++ lib/app/services/deep_link_service.dart | 18 ++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b6ffe5bd..ec226a5c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -28,6 +28,10 @@ android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> + diff --git a/lib/app/services/deep_link_service.dart b/lib/app/services/deep_link_service.dart index 5db40922..c80df04c 100644 --- a/lib/app/services/deep_link_service.dart +++ b/lib/app/services/deep_link_service.dart @@ -74,15 +74,17 @@ class DeepLinkService extends GetxService { } } else if (uri.host == "addclicked") { if (Get.context != null) { - Get.dialog( - Material( - child: AddTaskBottomSheet( - homeController: controller, - forTaskC: isTaskChampion, - forReplica: isReplica, + WidgetsBinding.instance.addPostFrameCallback((_) { + Get.dialog( + Material( + child: AddTaskBottomSheet( + homeController: controller, + forTaskC: isTaskChampion, + forReplica: isReplica, + ), ), - ), - ); + ); + }); } } }