From e32ec30a1e07bba6724456dd7ecb60a2f5f6aa79 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 3 Jan 2024 22:10:07 +0000 Subject: [PATCH] Fix Crash Bug in Android (ShareFile and Reconnect) + Force prettyDateString to use .toLocal() time + Formatting (#794) commit fe4726986f82104eaa9af5a0a0b8a35f8f0c60f0 Author: Sarah Jamie Lewis Date: Tue Jan 2 10:53:15 2024 -0800 Formatting commit d4e57f493ef323c5a09af7cb64afb31e0ad2963e Author: Sarah Jamie Lewis Date: Tue Jan 2 10:48:31 2024 -0800 Fix Crash Bug in Android (ShareFile and Reconnect) In rare situtaitons (exacerbated by debug mode and multiple file shares in succession) ReconnectCwtchForeground events can result in negative message counts being calculated in the UI. This fix ensures that doesn't happen, but a complete fix will need to wait until #664 is implement in the backend commit 44925783f559dee3ba3a182ce2e04214589b6bf1 Author: Sarah Jamie Lewis Date: Tue Jan 2 09:14:49 2024 -0800 Force prettyDateString to use .toLocal() time Fixes an issue where, on some platforms, contact row dates in non-streaming mode were displayed in UTC. Reviewed-on: https://git.openprivacy.ca/cwtch.im/cwtch-ui/pulls/794 Reviewed-by: Dan Ballard --- LIBCWTCH-GO.version | 2 +- .../kotlin/im/cwtch/flwtch/MainActivity.kt | 3 ++- lib/cwtch/cwtch.dart | 2 ++ lib/cwtch/ffi.dart | 9 ++++++++ lib/cwtch/gomobile.dart | 5 +++++ lib/main.dart | 6 +++++- lib/models/contact.dart | 11 ++++++++++ lib/models/contactlist.dart | 2 +- lib/models/messagecache.dart | 21 ++++++++++++------- lib/models/profile.dart | 17 ++++++++++++++- lib/models/redaction.dart | 2 +- lib/themes/opaque.dart | 2 +- lib/themes/yamltheme.dart | 16 +++++++------- lib/views/globalsettingsview.dart | 2 +- lib/views/messageview.dart | 1 + lib/views/peersettingsview.dart | 18 ++++++++++++++-- lib/widgets/messagelist.dart | 5 +++-- run-tests.sh | 12 +++++------ 18 files changed, 101 insertions(+), 35 deletions(-) diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index eeac635b..e6cb9215 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2023-09-26-13-15-v0.0.10 \ No newline at end of file +2024-01-03-20-52-v0.0.10-4-g6c0b2e2 \ No newline at end of file diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index d2eafd73..7d19a003 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -331,8 +331,9 @@ class MainActivity: FlutterActivity() { val conversation: Int = call.argument("conversation") ?: 0 val indexI: Int = call.argument("index") ?: 0 val count: Int = call.argument("count") ?: 1 + val ucount : Int = maxOf(1, count) // don't allow negative counts - result.success(Cwtch.getMessages(profile, conversation.toLong(), indexI.toLong(), count.toLong())) + result.success(Cwtch.getMessages(profile, conversation.toLong(), indexI.toLong(), ucount.toLong())) return } "SendMessage" -> { diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index 1cc06e22..b19d0f72 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -156,4 +156,6 @@ abstract class Cwtch { void DeleteServerInfo(String profile, String handle); void PublishServerUpdate(String onion); Future ConfigureConnections(String onion, bool listen, bool peers, bool servers); + + bool IsLoaded(); } diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index aeb81df9..059ff55d 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -268,6 +268,8 @@ class CwtchFfi implements Cwtch { // Called on object being disposed to (presumably on app close) to close the isolate that's listening to libcwtch-go events @override void dispose() { + EnvironmentConfig.debugLog("tearing down cwtch FFI isolate"); + library.close(); cwtchIsolate.kill(priority: Isolate.immediate); } @@ -1121,4 +1123,11 @@ class CwtchFfi implements Cwtch { PublishServerUpdate(utf8profile, utf8profile.length); malloc.free(utf8profile); } + + @override + bool IsLoaded() { + bool check = library.providesSymbol("c_UpdateSettings"); + EnvironmentConfig.debugLog("Checking that the FFI Interface is Correctly Loaded... $check"); + return check; + } } diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 4e9804f0..3eca7ecd 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -469,4 +469,9 @@ class CwtchGomobile implements Cwtch { void PublishServerUpdate(String profile) { cwtchPlatform.invokeMethod("PublishServerUpdate", {"ProfileOnion": profile}); } + + @override + bool IsLoaded() { + return true; + } } diff --git a/lib/main.dart b/lib/main.dart index d58ef0b7..45dd4b64 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -173,6 +173,10 @@ class FlwtchState extends State with WindowListener { getServerListStateProvider(), ], builder: (context, widget) { + // in test mode...rebuild everything every second...if cwtch isn't loaded... + if (EnvironmentConfig.TEST_MODE && cwtch.IsLoaded() == false) { + Timer t = new Timer.periodic(Duration(seconds: 1), (Timer t) => setState(() {})); + } return Consumer2( builder: (context, settings, appState, child) => MaterialApp( key: Key('app'), @@ -190,7 +194,7 @@ class FlwtchState extends State with WindowListener { title: 'Cwtch', showSemanticsDebugger: settings.useSemanticDebugger, theme: mkThemeData(settings), - home: (!appState.cwtchInit || appState.modalState != ModalState.none) ? SplashView() : ProfileMgrView(), + home: (!appState.cwtchInit || appState.modalState != ModalState.none) || !cwtch.IsLoaded() ? SplashView() : ProfileMgrView(), ), ); }, diff --git a/lib/models/contact.dart b/lib/models/contact.dart index e0931169..2dfae7f2 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -75,6 +75,8 @@ class ContactInfoState extends ChangeNotifier { DateTime _lastRetryTime = DateTime.now(); DateTime loaded = DateTime.now(); + List contactEvents = List.empty(growable: true); + ContactInfoState( this.profileOnion, this.identifier, @@ -198,6 +200,7 @@ class ContactInfoState extends ChangeNotifier { set status(String newVal) { this._status = newVal; + this.contactEvents.add(ContactEvent("Update Peer Status Received: $newVal")); notifyListeners(); } @@ -486,3 +489,11 @@ class ContactInfoState extends ChangeNotifier { } } } + +class ContactEvent { + String summary; + late DateTime timestamp; + ContactEvent(this.summary) { + this.timestamp = DateTime.now(); + } +} diff --git a/lib/models/contactlist.dart b/lib/models/contactlist.dart index ef8a0cf4..e8709508 100644 --- a/lib/models/contactlist.dart +++ b/lib/models/contactlist.dart @@ -98,7 +98,7 @@ class ContactListState extends ChangeNotifier { notifyListeners(); //} } - + void updateLastMessageReceivedTime(int forIdentifier, DateTime newMessageTime) { var contact = getContact(forIdentifier); if (contact == null) return; diff --git a/lib/models/messagecache.dart b/lib/models/messagecache.dart index b1807a0a..94bb834e 100644 --- a/lib/models/messagecache.dart +++ b/lib/models/messagecache.dart @@ -30,22 +30,27 @@ class LocalIndexMessage { this.messageId = messageId; this.cacheOnly = cacheOnly; this.isLoading = isLoading; - if (isLoading) { - loader = Completer(); - loaded = loader.future; + loader = Completer(); + loaded = loader.future; + if (!isLoading) { + loader.complete(); // complete this } } void finishLoad(int messageId) { this.messageId = messageId; - isLoading = false; - loader.complete(true); + if (!loader.isCompleted) { + isLoading = false; + loader.complete(true); + } } void failLoad() { this.messageId = null; - isLoading = false; - loader.complete(true); + if (!loader.isCompleted) { + isLoading = false; + loader.complete(true); + } } Future waitForLoad() { @@ -95,7 +100,7 @@ class MessageCache extends ChangeNotifier { this._storageMessageCount = newval; } - // On android reconnect, if backend supplied message count > UI message count, add the differnce to the front of the index + // On android reconnect, if backend supplied message count > UI message count, add the difference to the front of the index void addFrontIndexGap(int count) { this._indexUnsynced = count; } diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 3269f25a..a1e62ac3 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'package:cwtch/config.dart'; import 'package:cwtch/models/remoteserver.dart'; @@ -248,8 +249,22 @@ class ProfileInfoState extends ChangeNotifier { if (profileContact != null) { profileContact.status = contact["status"]; - var newCount = contact["numMessages"]; + var newCount = contact["numMessages"] as int; if (newCount != profileContact.totalMessages) { + if (newCount < profileContact.totalMessages) { + // on Android, when sharing a file the UI may be briefly unloaded for the + // OS to display the file management/selection screen. Afterwards a + // call to ReconnectCwtchForeground will be made which will refresh all values (including count of numMessages) + // **at the same time** the foreground will increment .totalMessages and send a new message to the backend. + // This will result in a negative number of messages being calculated here, and an incorrect totalMessage count. + // This bug is exacerbated in debug mode, and when multiple files are sent in succession. Both cases result in multiple ReconnectCwtchForeground + // events that have the potential to conflict with currentMessageCounts. + // Note that *if* a new message came in at the same time, we would be unable to distinguish this case - as such this is specific instance of a more general problem + // TODO: A true-fix to this bug is to implement a syncing step in the foreground where totalMessages and inFlightMessages can be distinguished + // This requires a change to the backend to confirm submission of an inFlightMessage, which will be implemented in #664 + EnvironmentConfig.debugLog("Conflicting message counts: $newCount ${profileContact.totalMessages}"); + newCount = max(newCount, profileContact.totalMessages); + } profileContact.messageCache.addFrontIndexGap(newCount - profileContact.totalMessages); } profileContact.totalMessages = newCount; diff --git a/lib/models/redaction.dart b/lib/models/redaction.dart index ab62a08d..2fb3b2fd 100644 --- a/lib/models/redaction.dart +++ b/lib/models/redaction.dart @@ -39,5 +39,5 @@ String prettyDateString(BuildContext context, DateTime date) { // } return AppLocalizations.of(context)!.now; } - return DateFormat.yMd(Platform.localeName).add_jm().format(date); + return DateFormat.yMd(Platform.localeName).add_jm().format(date.toLocal()); } diff --git a/lib/themes/opaque.dart b/lib/themes/opaque.dart index 74fdca15..e012c730 100644 --- a/lib/themes/opaque.dart +++ b/lib/themes/opaque.dart @@ -24,7 +24,7 @@ LoadAssetThemes() async { themes = await loadYamlThemes(); } -OpaqueThemeType getTheme(String themeId, String mode) { +OpaqueThemeType getTheme(String themeId, String mode) { if (themeId == "") { themeId = cwtch_theme; } diff --git a/lib/themes/yamltheme.dart b/lib/themes/yamltheme.dart index c788d50b..98e2304b 100644 --- a/lib/themes/yamltheme.dart +++ b/lib/themes/yamltheme.dart @@ -8,8 +8,6 @@ import 'package:flutter/services.dart'; import 'package:yaml/yaml.dart'; import 'package:path/path.dart' as path; - - Future>> loadYamlThemes() async { final manifestJson = await rootBundle.loadString('AssetManifest.json'); final themesList = json.decode(manifestJson).keys.where((String key) => key.startsWith('assets/themes')); @@ -17,7 +15,7 @@ Future>> loadYamlThemes() async { Map> themes = Map(); for (String themefile in themesList) { - if (themefile.substring(themefile.length-4) != ".yml") { + if (themefile.substring(themefile.length - 4) != ".yml") { continue; } @@ -77,16 +75,16 @@ class YmlTheme extends OpaqueThemeType { Color? getColor(String name) { var val = yml["themes"][mode]["theme"][name]; - if (! (val is int)) { + if (!(val is int)) { val = yml["themes"][mode]["theme"][val] ?? val; } - if (! (val is int)) { + if (!(val is int)) { val = yml["themes"][mode]?["colors"][val] ?? val; } - if (! (val is int)) { + if (!(val is int)) { val = yml["colors"]?[val]; } - if (! (val is int)) { + if (!(val is int)) { return null; } return Color(0xFF000000 + val as int); @@ -95,7 +93,7 @@ class YmlTheme extends OpaqueThemeType { String? getImage(String name) { var val = yml["themes"][mode]["theme"]?[name]; if (val != null) { - return path.join("assets", "themes", yml["themes"]["name"], val); + return path.join("assets", "themes", yml["themes"]["name"], val); } return null; } @@ -137,4 +135,4 @@ class YmlTheme extends OpaqueThemeType { // Images get chatImage => getImage("chatImage") ?? fallbackTheme.chatImage; -} \ No newline at end of file +} diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index 2a6062cc..69829d6f 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -83,7 +83,7 @@ class _GlobalSettingsViewState extends State { } Widget _buildSettingsList() { - return Consumer(builder: (context, settings, child) { + return Consumer(builder: (ccontext, settings, child) { return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { var appIcon = Icon(Icons.info, color: settings.current().mainTextColor); return Scrollbar( diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index c5044d9c..0f5a18a7 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -125,6 +125,7 @@ class _MessageViewState extends State { } // reset the disconnect button to allow for immediate connection... Provider.of(context, listen: false).lastRetryTime = DateTime.now().subtract(Duration(minutes: 2)); + Provider.of(context, listen: false).contactEvents.add(ContactEvent("Disconnect from Peer")); })); } diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 8e1fe04c..ff307427 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:ui'; +import 'package:cwtch/config.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/models/appstate.dart'; import 'package:cwtch/models/contact.dart'; @@ -84,6 +85,13 @@ class _PeerSettingsViewState extends State { } } + List evWidgets = Provider.of(context, listen: false).contactEvents.map((ev) { + return ListTile( + title: Text(ev.summary, style: Provider.of(context).scaleFonts(defaultTextStyle)), + subtitle: Text(ev.timestamp.toLocal().toIso8601String(), style: Provider.of(context).scaleFonts(defaultTextStyle)), + ); + }).toList(); + return Scrollbar( trackVisibility: true, controller: peerSettingsScrollController, @@ -319,8 +327,14 @@ class _PeerSettingsViewState extends State { style: settings.scaleFonts(defaultTextButtonStyle.copyWith(decoration: TextDecoration.underline)), ), )) - ]) - ]) + ]), + ]), + Visibility( + visible: EnvironmentConfig.BUILD_VER == dev_version, + maintainSize: false, + child: Column( + children: evWidgets, + )) ]))))); }); }); diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 75623952..e71d01e0 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -75,6 +75,7 @@ class _MessageListState extends State { .AttemptReconnection(Provider.of(context, listen: false).onion, Provider.of(context, listen: false).onion); } Provider.of(context, listen: false).lastRetryTime = DateTime.now(); + Provider.of(context, listen: false).contactEvents.add(ContactEvent("Actively Retried Connection")); setState(() { // force update of this view...otherwise the button won't be removed fast enough... }); @@ -116,8 +117,8 @@ class _MessageListState extends State { // Only show broken heart is the contact is offline... decoration: BoxDecoration( image: Provider.of(outerContext).isOnline() - ? (Provider.of(context).theme.chatImage != null) ? - DecorationImage( + ? (Provider.of(context).theme.chatImage != null) + ? DecorationImage( repeat: ImageRepeat.repeat, image: AssetImage(Provider.of(context).theme.chatImage), colorFilter: ColorFilter.mode(Provider.of(context).theme.hilightElementColor.withOpacity(0.15), BlendMode.srcIn)) diff --git a/run-tests.sh b/run-tests.sh index eb9c68c5..a3b3b696 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -5,11 +5,11 @@ sed "s|featurePaths: REPLACED_BY_SCRIPT|featurePaths: [$paths]|" integra flutter pub run build_runner clean flutter pub run build_runner build --delete-conflicting-outputs -PATH=$PATH:$PWD/linux/Tor -LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"$PWD/linux/":"$PWD/linux/Tor/" -PATH=$PATH LD_LIBRARY_PATH=$LD_LIBRARY_PATH LOG_FILE=test.log CWTCH_HOME=$PWD/integration_test/env/temp/ flutter test -d linux --dart-define TEST_MODE=true integration_test/gherkin_suite_test.dart -#node index2.js -#if [ "$HEADLESS" = "false" ]; then +PATH=$PATH:"$PWD/linux/Tor" +LD_LIBRARY_PATH="$PWD/linux/":"$PWD/linux/Tor/":$LD_LIBRARY_PATH +env PATH=$PATH LD_LIBRARY_PATH=$LD_LIBRARY_PATH LOG_FILE=test.log CWTCH_HOME=$PWD/integration_test/env/temp/ flutter test -d linux --dart-define TEST_MODE=true integration_test/gherkin_suite_test.dart +# node index2.js +# if [ "$HEADLESS" = "false" ]; then # xdg-open integration_test/gherkin/reports/cucumber_report.html -#fi +# fi