Fix Crash Bug in Android (ShareFile and Reconnect) + Force prettyDateString to use .toLocal() time + Formatting #794

Merged
sarah merged 9 commits from post-stable-fixes into trunk 2024-01-03 22:10:09 +00:00
18 changed files with 101 additions and 35 deletions

View File

@ -1 +1 @@
2023-09-26-13-15-v0.0.10 2024-01-03-20-52-v0.0.10-4-g6c0b2e2

View File

@ -331,8 +331,9 @@ class MainActivity: FlutterActivity() {
val conversation: Int = call.argument("conversation") ?: 0 val conversation: Int = call.argument("conversation") ?: 0
val indexI: Int = call.argument("index") ?: 0 val indexI: Int = call.argument("index") ?: 0
val count: Int = call.argument("count") ?: 1 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 return
} }
"SendMessage" -> { "SendMessage" -> {

View File

@ -156,4 +156,6 @@ abstract class Cwtch {
void DeleteServerInfo(String profile, String handle); void DeleteServerInfo(String profile, String handle);
void PublishServerUpdate(String onion); void PublishServerUpdate(String onion);
Future<void> ConfigureConnections(String onion, bool listen, bool peers, bool servers); Future<void> ConfigureConnections(String onion, bool listen, bool peers, bool servers);
bool IsLoaded();
} }

View File

@ -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 // Called on object being disposed to (presumably on app close) to close the isolate that's listening to libcwtch-go events
@override @override
void dispose() { void dispose() {
EnvironmentConfig.debugLog("tearing down cwtch FFI isolate");
library.close();
cwtchIsolate.kill(priority: Isolate.immediate); cwtchIsolate.kill(priority: Isolate.immediate);
} }
@ -1121,4 +1123,11 @@ class CwtchFfi implements Cwtch {
PublishServerUpdate(utf8profile, utf8profile.length); PublishServerUpdate(utf8profile, utf8profile.length);
malloc.free(utf8profile); 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;
}
} }

View File

@ -469,4 +469,9 @@ class CwtchGomobile implements Cwtch {
void PublishServerUpdate(String profile) { void PublishServerUpdate(String profile) {
cwtchPlatform.invokeMethod("PublishServerUpdate", {"ProfileOnion": profile}); cwtchPlatform.invokeMethod("PublishServerUpdate", {"ProfileOnion": profile});
} }
@override
bool IsLoaded() {
return true;
}
} }

View File

@ -173,6 +173,10 @@ class FlwtchState extends State<Flwtch> with WindowListener {
getServerListStateProvider(), getServerListStateProvider(),
], ],
builder: (context, widget) { 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<Settings, AppState>( return Consumer2<Settings, AppState>(
builder: (context, settings, appState, child) => MaterialApp( builder: (context, settings, appState, child) => MaterialApp(
key: Key('app'), key: Key('app'),
@ -190,7 +194,7 @@ class FlwtchState extends State<Flwtch> with WindowListener {
title: 'Cwtch', title: 'Cwtch',
showSemanticsDebugger: settings.useSemanticDebugger, showSemanticsDebugger: settings.useSemanticDebugger,
theme: mkThemeData(settings), theme: mkThemeData(settings),
home: (!appState.cwtchInit || appState.modalState != ModalState.none) ? SplashView() : ProfileMgrView(), home: (!appState.cwtchInit || appState.modalState != ModalState.none) || !cwtch.IsLoaded() ? SplashView() : ProfileMgrView(),
), ),
); );
}, },

View File

@ -75,6 +75,8 @@ class ContactInfoState extends ChangeNotifier {
DateTime _lastRetryTime = DateTime.now(); DateTime _lastRetryTime = DateTime.now();
DateTime loaded = DateTime.now(); DateTime loaded = DateTime.now();
List<ContactEvent> contactEvents = List.empty(growable: true);
ContactInfoState( ContactInfoState(
this.profileOnion, this.profileOnion,
this.identifier, this.identifier,
@ -198,6 +200,7 @@ class ContactInfoState extends ChangeNotifier {
set status(String newVal) { set status(String newVal) {
this._status = newVal; this._status = newVal;
this.contactEvents.add(ContactEvent("Update Peer Status Received: $newVal"));
notifyListeners(); notifyListeners();
} }
@ -486,3 +489,11 @@ class ContactInfoState extends ChangeNotifier {
} }
} }
} }
class ContactEvent {
String summary;
late DateTime timestamp;
ContactEvent(this.summary) {
this.timestamp = DateTime.now();
}
}

View File

@ -98,7 +98,7 @@ class ContactListState extends ChangeNotifier {
notifyListeners(); notifyListeners();
//} </todo> //} </todo>
} }
void updateLastMessageReceivedTime(int forIdentifier, DateTime newMessageTime) { void updateLastMessageReceivedTime(int forIdentifier, DateTime newMessageTime) {
var contact = getContact(forIdentifier); var contact = getContact(forIdentifier);
if (contact == null) return; if (contact == null) return;

View File

@ -30,22 +30,27 @@ class LocalIndexMessage {
this.messageId = messageId; this.messageId = messageId;
this.cacheOnly = cacheOnly; this.cacheOnly = cacheOnly;
this.isLoading = isLoading; this.isLoading = isLoading;
if (isLoading) { loader = Completer<void>();
loader = Completer<void>(); loaded = loader.future;
loaded = loader.future; if (!isLoading) {
loader.complete(); // complete this
} }
} }
void finishLoad(int messageId) { void finishLoad(int messageId) {
this.messageId = messageId; this.messageId = messageId;
isLoading = false; if (!loader.isCompleted) {
loader.complete(true); isLoading = false;
loader.complete(true);
}
} }
void failLoad() { void failLoad() {
this.messageId = null; this.messageId = null;
isLoading = false; if (!loader.isCompleted) {
loader.complete(true); isLoading = false;
loader.complete(true);
}
} }
Future<void> waitForLoad() { Future<void> waitForLoad() {
@ -95,7 +100,7 @@ class MessageCache extends ChangeNotifier {
this._storageMessageCount = newval; 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) { void addFrontIndexGap(int count) {
this._indexUnsynced = count; this._indexUnsynced = count;
} }

View File

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'package:cwtch/config.dart'; import 'package:cwtch/config.dart';
import 'package:cwtch/models/remoteserver.dart'; import 'package:cwtch/models/remoteserver.dart';
@ -248,8 +249,22 @@ class ProfileInfoState extends ChangeNotifier {
if (profileContact != null) { if (profileContact != null) {
profileContact.status = contact["status"]; profileContact.status = contact["status"];
var newCount = contact["numMessages"]; var newCount = contact["numMessages"] as int;
if (newCount != profileContact.totalMessages) { 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.messageCache.addFrontIndexGap(newCount - profileContact.totalMessages);
} }
profileContact.totalMessages = newCount; profileContact.totalMessages = newCount;

View File

@ -39,5 +39,5 @@ String prettyDateString(BuildContext context, DateTime date) {
// } // }
return AppLocalizations.of(context)!.now; return AppLocalizations.of(context)!.now;
} }
return DateFormat.yMd(Platform.localeName).add_jm().format(date); return DateFormat.yMd(Platform.localeName).add_jm().format(date.toLocal());
} }

View File

@ -24,7 +24,7 @@ LoadAssetThemes() async {
themes = await loadYamlThemes(); themes = await loadYamlThemes();
} }
OpaqueThemeType getTheme(String themeId, String mode) { OpaqueThemeType getTheme(String themeId, String mode) {
if (themeId == "") { if (themeId == "") {
themeId = cwtch_theme; themeId = cwtch_theme;
} }

View File

@ -8,8 +8,6 @@ import 'package:flutter/services.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
Future<Map<String, Map<String, OpaqueThemeType>>> loadYamlThemes() async { Future<Map<String, Map<String, OpaqueThemeType>>> loadYamlThemes() async {
final manifestJson = await rootBundle.loadString('AssetManifest.json'); final manifestJson = await rootBundle.loadString('AssetManifest.json');
final themesList = json.decode(manifestJson).keys.where((String key) => key.startsWith('assets/themes')); final themesList = json.decode(manifestJson).keys.where((String key) => key.startsWith('assets/themes'));
@ -17,7 +15,7 @@ Future<Map<String, Map<String, OpaqueThemeType>>> loadYamlThemes() async {
Map<String, Map<String, OpaqueThemeType>> themes = Map(); Map<String, Map<String, OpaqueThemeType>> themes = Map();
for (String themefile in themesList) { for (String themefile in themesList) {
if (themefile.substring(themefile.length-4) != ".yml") { if (themefile.substring(themefile.length - 4) != ".yml") {
continue; continue;
} }
@ -77,16 +75,16 @@ class YmlTheme extends OpaqueThemeType {
Color? getColor(String name) { Color? getColor(String name) {
var val = yml["themes"][mode]["theme"][name]; var val = yml["themes"][mode]["theme"][name];
if (! (val is int)) { if (!(val is int)) {
val = yml["themes"][mode]["theme"][val] ?? val; val = yml["themes"][mode]["theme"][val] ?? val;
} }
if (! (val is int)) { if (!(val is int)) {
val = yml["themes"][mode]?["colors"][val] ?? val; val = yml["themes"][mode]?["colors"][val] ?? val;
} }
if (! (val is int)) { if (!(val is int)) {
val = yml["colors"]?[val]; val = yml["colors"]?[val];
} }
if (! (val is int)) { if (!(val is int)) {
return null; return null;
} }
return Color(0xFF000000 + val as int); return Color(0xFF000000 + val as int);
@ -95,7 +93,7 @@ class YmlTheme extends OpaqueThemeType {
String? getImage(String name) { String? getImage(String name) {
var val = yml["themes"][mode]["theme"]?[name]; var val = yml["themes"][mode]["theme"]?[name];
if (val != null) { if (val != null) {
return path.join("assets", "themes", yml["themes"]["name"], val); return path.join("assets", "themes", yml["themes"]["name"], val);
} }
return null; return null;
} }
@ -137,4 +135,4 @@ class YmlTheme extends OpaqueThemeType {
// Images // Images
get chatImage => getImage("chatImage") ?? fallbackTheme.chatImage; get chatImage => getImage("chatImage") ?? fallbackTheme.chatImage;
} }

View File

@ -83,7 +83,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
} }
Widget _buildSettingsList() { Widget _buildSettingsList() {
return Consumer<Settings>(builder: (context, settings, child) { return Consumer<Settings>(builder: (ccontext, settings, child) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) {
var appIcon = Icon(Icons.info, color: settings.current().mainTextColor); var appIcon = Icon(Icons.info, color: settings.current().mainTextColor);
return Scrollbar( return Scrollbar(

View File

@ -125,6 +125,7 @@ class _MessageViewState extends State<MessageView> {
} }
// reset the disconnect button to allow for immediate connection... // reset the disconnect button to allow for immediate connection...
Provider.of<ContactInfoState>(context, listen: false).lastRetryTime = DateTime.now().subtract(Duration(minutes: 2)); Provider.of<ContactInfoState>(context, listen: false).lastRetryTime = DateTime.now().subtract(Duration(minutes: 2));
Provider.of<ContactInfoState>(context, listen: false).contactEvents.add(ContactEvent("Disconnect from Peer"));
})); }));
} }

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:ui'; import 'dart:ui';
import 'package:cwtch/config.dart';
import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/appstate.dart'; import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/contact.dart';
@ -84,6 +85,13 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
} }
} }
List<Widget> evWidgets = Provider.of<ContactInfoState>(context, listen: false).contactEvents.map((ev) {
return ListTile(
title: Text(ev.summary, style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle)),
subtitle: Text(ev.timestamp.toLocal().toIso8601String(), style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle)),
);
}).toList();
return Scrollbar( return Scrollbar(
trackVisibility: true, trackVisibility: true,
controller: peerSettingsScrollController, controller: peerSettingsScrollController,
@ -319,8 +327,14 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
style: settings.scaleFonts(defaultTextButtonStyle.copyWith(decoration: TextDecoration.underline)), style: settings.scaleFonts(defaultTextButtonStyle.copyWith(decoration: TextDecoration.underline)),
), ),
)) ))
]) ]),
]) ]),
Visibility(
visible: EnvironmentConfig.BUILD_VER == dev_version,
maintainSize: false,
child: Column(
children: evWidgets,
))
]))))); ])))));
}); });
}); });

View File

@ -75,6 +75,7 @@ class _MessageListState extends State<MessageList> {
.AttemptReconnection(Provider.of<ProfileInfoState>(context, listen: false).onion, Provider.of<ContactInfoState>(context, listen: false).onion); .AttemptReconnection(Provider.of<ProfileInfoState>(context, listen: false).onion, Provider.of<ContactInfoState>(context, listen: false).onion);
} }
Provider.of<ContactInfoState>(context, listen: false).lastRetryTime = DateTime.now(); Provider.of<ContactInfoState>(context, listen: false).lastRetryTime = DateTime.now();
Provider.of<ContactInfoState>(context, listen: false).contactEvents.add(ContactEvent("Actively Retried Connection"));
setState(() { setState(() {
// force update of this view...otherwise the button won't be removed fast enough... // force update of this view...otherwise the button won't be removed fast enough...
}); });
@ -116,8 +117,8 @@ class _MessageListState extends State<MessageList> {
// Only show broken heart is the contact is offline... // Only show broken heart is the contact is offline...
decoration: BoxDecoration( decoration: BoxDecoration(
image: Provider.of<ContactInfoState>(outerContext).isOnline() image: Provider.of<ContactInfoState>(outerContext).isOnline()
? (Provider.of<Settings>(context).theme.chatImage != null) ? ? (Provider.of<Settings>(context).theme.chatImage != null)
DecorationImage( ? DecorationImage(
repeat: ImageRepeat.repeat, repeat: ImageRepeat.repeat,
image: AssetImage(Provider.of<Settings>(context).theme.chatImage), image: AssetImage(Provider.of<Settings>(context).theme.chatImage),
colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementColor.withOpacity(0.15), BlendMode.srcIn)) colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementColor.withOpacity(0.15), BlendMode.srcIn))

View File

@ -5,11 +5,11 @@ sed "s|featurePaths: REPLACED_BY_SCRIPT|featurePaths: <String>[$paths]|" integra
flutter pub run build_runner clean flutter pub run build_runner clean
flutter pub run build_runner build --delete-conflicting-outputs flutter pub run build_runner build --delete-conflicting-outputs
PATH=$PATH:$PWD/linux/Tor PATH=$PATH:"$PWD/linux/Tor"
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"$PWD/linux/":"$PWD/linux/Tor/" LD_LIBRARY_PATH="$PWD/linux/":"$PWD/linux/Tor/":$LD_LIBRARY_PATH
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 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 # node index2.js
#if [ "$HEADLESS" = "false" ]; then # if [ "$HEADLESS" = "false" ]; then
# xdg-open integration_test/gherkin/reports/cucumber_report.html # xdg-open integration_test/gherkin/reports/cucumber_report.html
#fi # fi