Fix Crash Bug in Android (ShareFile and Reconnect) + Force prettyDateString to use .toLocal() time + Formatting (#794)
continuous-integration/drone/push Build is passing Details

commit fe4726986f
Author: Sarah Jamie Lewis <sarah@openprivacy.ca>
Date:   Tue Jan 2 10:53:15 2024 -0800

    Formatting

commit d4e57f493e
Author: Sarah Jamie Lewis <sarah@openprivacy.ca>
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 44925783f5
Author: Sarah Jamie Lewis <sarah@openprivacy.ca>
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: #794
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
This commit is contained in:
Sarah Jamie Lewis 2024-01-03 22:10:07 +00:00
parent e421642a02
commit e32ec30a1e
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 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" -> {

View File

@ -156,4 +156,6 @@ abstract class Cwtch {
void DeleteServerInfo(String profile, String handle);
void PublishServerUpdate(String onion);
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
@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;
}
}

View File

@ -469,4 +469,9 @@ class CwtchGomobile implements Cwtch {
void PublishServerUpdate(String 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(),
],
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>(
builder: (context, settings, appState, child) => MaterialApp(
key: Key('app'),
@ -190,7 +194,7 @@ class FlwtchState extends State<Flwtch> 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(),
),
);
},

View File

@ -75,6 +75,8 @@ class ContactInfoState extends ChangeNotifier {
DateTime _lastRetryTime = DateTime.now();
DateTime loaded = DateTime.now();
List<ContactEvent> 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();
}
}

View File

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

View File

@ -30,22 +30,27 @@ class LocalIndexMessage {
this.messageId = messageId;
this.cacheOnly = cacheOnly;
this.isLoading = isLoading;
if (isLoading) {
loader = Completer<void>();
loaded = loader.future;
loader = Completer<void>();
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<void> 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;
}

View File

@ -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;

View File

@ -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());
}

View File

@ -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;
}

View File

@ -8,8 +8,6 @@ import 'package:flutter/services.dart';
import 'package:yaml/yaml.dart';
import 'package:path/path.dart' as path;
Future<Map<String, Map<String, OpaqueThemeType>>> 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<Map<String, Map<String, OpaqueThemeType>>> loadYamlThemes() async {
Map<String, Map<String, OpaqueThemeType>> 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;
}
}

View File

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

View File

@ -125,6 +125,7 @@ class _MessageViewState extends State<MessageView> {
}
// 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).contactEvents.add(ContactEvent("Disconnect from Peer"));
}));
}

View File

@ -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<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(
trackVisibility: true,
controller: peerSettingsScrollController,
@ -319,8 +327,14 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
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);
}
Provider.of<ContactInfoState>(context, listen: false).lastRetryTime = DateTime.now();
Provider.of<ContactInfoState>(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<MessageList> {
// Only show broken heart is the contact is offline...
decoration: BoxDecoration(
image: Provider.of<ContactInfoState>(outerContext).isOnline()
? (Provider.of<Settings>(context).theme.chatImage != null) ?
DecorationImage(
? (Provider.of<Settings>(context).theme.chatImage != null)
? DecorationImage(
repeat: ImageRepeat.repeat,
image: AssetImage(Provider.of<Settings>(context).theme.chatImage),
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 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