diff --git a/.github/workflows/dev_github.yml b/.github/workflows/dev_github.yml index 6284a37..3618924 100644 --- a/.github/workflows/dev_github.yml +++ b/.github/workflows/dev_github.yml @@ -25,8 +25,11 @@ jobs: - name: Cloning sub-repos run: git submodule update --init --recursive - - name: Check flutter code - run: | - flutter pub get - flutter analyze - flutter test + - name: flutter pub get + run: flutter pub get + + - name: flutter analyze + run: flutter analyze + + - name: flutter test + run: flutter test diff --git a/CHANGELOG.md b/CHANGELOG.md index 2de426e..458a935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.0.98 +## 0.0.99 - New: Groups can now collect flames as well - New: Background execution to pre-load messages @@ -8,10 +8,12 @@ - Improve: Video compression with progress updates - Improve: Show message "Flames restored" - Improve: Show toast message if user was added via QR +- Fix: Media file appears as a white square and is not listed. - Fix: Issue with media files required to be reuploaded - Fix: Problem during contact requests - Fix: Problem with deleting a contact - Fix: Problem with restoring from backup +- Fix: Issue with the log file ## 0.0.96 diff --git a/lib/main.dart b/lib/main.dart index 84249c4..f148c74 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,6 +29,10 @@ import 'package:twonly/src/utils/storage.dart'; void main() async { SentryWidgetsFlutterBinding.ensureInitialized(); + globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path; + globalApplicationSupportDirectory = + (await getApplicationSupportDirectory()).path; + await initFCMService(); final user = await getUser(); @@ -53,10 +57,6 @@ void main() async { await deleteLocalUserData(); } - globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path; - globalApplicationSupportDirectory = - (await getApplicationSupportDirectory()).path; - initLogger(); final settingsController = SettingsChangeProvider(); diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index 22aea85..ffa96b5 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -1,6 +1,7 @@ import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/receipts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -9,7 +10,9 @@ import 'package:twonly/src/utils/log.dart'; part 'receipts.dao.g.dart'; -@DriftAccessor(tables: [Receipts, Messages, MessageActions, ReceivedReceipts]) +@DriftAccessor( + tables: [Receipts, Messages, MessageActions, ReceivedReceipts, Contacts], +) class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { // this constructor is required so that the main database can create an instance // of this object. @@ -60,6 +63,17 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { )), )) .go(); + + final deletedContacts = await (select( + contacts, + )..where((t) => t.accountDeleted.equals(true))).get(); + + for (final contact in deletedContacts) { + await (delete(receipts)..where( + (t) => t.contactId.equals(contact.userId), + )) + .go(); + } } Future insertReceipt(ReceiptsCompanion entry) async { diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 8f2b606..cac8dd2 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -46,6 +46,7 @@ import 'package:web_socket_channel/io.dart'; final lockConnecting = Mutex(); final lockRetransStore = Mutex(); +final lockAuthentication = Mutex(); /// The ApiProvider is responsible for communicating with the server. /// It handles errors and does automatically tries to reconnect on @@ -86,7 +87,6 @@ class ApiService { // Function is called after the user is authenticated at the server Future onAuthenticated() async { - isAuthenticated = true; await initFCMAfterAuthenticated(); globalCallbackConnectionState(isConnected: true); @@ -157,8 +157,9 @@ class ApiService { if (connectivitySubscription != null) { return; } - connectivitySubscription = - Connectivity().onConnectivityChanged.listen((result) async { + connectivitySubscription = Connectivity().onConnectivityChanged.listen(( + result, + ) async { if (!result.contains(ConnectivityResult.none)) { await connect(); } @@ -355,7 +356,6 @@ class ApiService { return Result.error(ErrorCode.InternalError); } if (res.error == ErrorCode.SessionNotAuthenticated) { - isAuthenticated = false; if (authenticated) { await authenticate(); if (isAuthenticated) { @@ -387,8 +387,9 @@ class ApiService { Future tryAuthenticateWithToken(int userId) async { const storage = FlutterSecureStorage(); - final apiAuthToken = - await storage.read(key: SecureStorageKeys.apiAuthToken); + final apiAuthToken = await storage.read( + key: SecureStorageKeys.apiAuthToken, + ); final user = await getUser(); if (apiAuthToken != null && user != null) { @@ -412,6 +413,7 @@ class ApiService { if (result.isSuccess) { Log.info('websocket is authenticated'); + isAuthenticated = true; if (globalIsInBackgroundTask) { await onAuthenticated(); } else { @@ -433,60 +435,66 @@ class ApiService { } Future authenticate() async { - if (isAuthenticated) return; - if (await getSignalIdentity() == null) { - return; - } + return lockAuthentication.protect(() async { + if (isAuthenticated) return; + if (await getSignalIdentity() == null) { + return; + } - final userData = await getUser(); - if (userData == null) return; + final userData = await getUser(); + if (userData == null) return; - if (await tryAuthenticateWithToken(userData.userId)) { - return; - } + if (await tryAuthenticateWithToken(userData.userId)) { + return; + } - final handshake = Handshake() - ..getAuthChallenge = Handshake_GetAuthChallenge(); - final req = createClientToServerFromHandshake(handshake); + final handshake = Handshake() + ..getAuthChallenge = Handshake_GetAuthChallenge(); + final req = createClientToServerFromHandshake(handshake); - final result = await sendRequestSync(req, authenticated: false); - if (result.isError) { - Log.warn('could not request auth challenge', result); - return; - } + final result = await sendRequestSync(req, authenticated: false); + if (result.isError) { + Log.warn('could not request auth challenge', result); + return; + } - final challenge = result.value.authchallenge; + final challenge = result.value.authchallenge; - var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey(); - if (privKey == null) return; - final random = getRandomUint8List(32); - final signature = sign(privKey.serialize(), challenge as Uint8List, random); - privKey = null; + var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey(); + if (privKey == null) return; + final random = getRandomUint8List(32); + final signature = sign( + privKey.serialize(), + challenge as Uint8List, + random, + ); + privKey = null; - final getAuthToken = Handshake_GetAuthToken() - ..response = signature - ..userId = Int64(userData.userId); + final getAuthToken = Handshake_GetAuthToken() + ..response = signature + ..userId = Int64(userData.userId); - final getauthtoken = Handshake()..getAuthToken = getAuthToken; + final getauthtoken = Handshake()..getAuthToken = getAuthToken; - final req2 = createClientToServerFromHandshake(getauthtoken); + final req2 = createClientToServerFromHandshake(getauthtoken); - final result2 = await sendRequestSync(req2, authenticated: false); - if (result2.isError) { - Log.error('could not send auth response: ${result2.error}'); - return; - } + final result2 = await sendRequestSync(req2, authenticated: false); + if (result2.isError) { + Log.error('could not send auth response: ${result2.error}'); + return; + } - final apiAuthToken = result2.value.authtoken as Uint8List; - final apiAuthTokenB64 = base64Encode(apiAuthToken); + final apiAuthToken = result2.value.authtoken as Uint8List; + final apiAuthTokenB64 = base64Encode(apiAuthToken); - const storage = FlutterSecureStorage(); - await storage.write( - key: SecureStorageKeys.apiAuthToken, - value: apiAuthTokenB64, - ); + const storage = FlutterSecureStorage(); + await storage.write( + key: SecureStorageKeys.apiAuthToken, + value: apiAuthTokenB64, + ); - await tryAuthenticateWithToken(userData.userId); + await tryAuthenticateWithToken(userData.userId); + }); } Future register( @@ -505,8 +513,9 @@ class ApiService { final register = Handshake_Register() ..username = username - ..publicIdentityKey = - (await signalStore.getIdentityKeyPair()).getPublicKey().serialize() + ..publicIdentityKey = (await signalStore.getIdentityKeyPair()) + .getPublicKey() + .serialize() ..registrationId = Int64(signalIdentity.registrationId) ..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize() ..signedPrekeySignature = signedPreKey.signature @@ -526,8 +535,10 @@ class ApiService { } Future checkForDeletedUsernames() async { - final users = await twonlyDB.contactsDao - .getContactsByUsername('[deleted]', username2: '[Unknown]'); + final users = await twonlyDB.contactsDao.getContactsByUsername( + '[deleted]', + username2: '[Unknown]', + ); for (final user in users) { final userData = await getUserById(user.userId); if (userData != null) { diff --git a/lib/src/services/api/client2client/media.c2c.dart b/lib/src/services/api/client2client/media.c2c.dart index 9ea1886..0369eb9 100644 --- a/lib/src/services/api/client2client/media.c2c.dart +++ b/lib/src/services/api/client2client/media.c2c.dart @@ -4,7 +4,8 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + hide Message; import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/utils.dart'; @@ -31,7 +32,7 @@ Future handleMedia( message.senderId != fromUserId || message.mediaId == null) { Log.warn( - 'Got reupload for a message that either does not exists or sender != fromUserId or not a media file', + 'Got reupload from $fromUserId for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}', ); return; } @@ -53,8 +54,9 @@ Future handleMedia( ), ); - final mediaFile = - await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); + final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( + message.mediaId!, + ); if (mediaFile != null) { unawaited(startDownloadMedia(mediaFile, false)); @@ -89,56 +91,64 @@ Future handleMedia( } } - final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( - MediaFilesCompanion( - downloadState: const Value(DownloadState.pending), - type: Value(mediaType), - requiresAuthentication: Value(media.requiresAuthentication), - displayLimitInMilliseconds: Value( - displayLimitInMilliseconds, - ), - downloadToken: Value(Uint8List.fromList(media.downloadToken)), - encryptionKey: Value(Uint8List.fromList(media.encryptionKey)), - encryptionMac: Value(Uint8List.fromList(media.encryptionMac)), - encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)), - createdAt: Value(fromTimestamp(media.timestamp)), - ), - ); + late MediaFile? mediaFile; + late Message? message; - if (mediaFile == null) { - Log.error('Could not insert media file into database'); - return; - } + await twonlyDB.transaction(() async { + mediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + downloadState: const Value(DownloadState.pending), + type: Value(mediaType), + requiresAuthentication: Value(media.requiresAuthentication), + displayLimitInMilliseconds: Value( + displayLimitInMilliseconds, + ), + downloadToken: Value(Uint8List.fromList(media.downloadToken)), + encryptionKey: Value(Uint8List.fromList(media.encryptionKey)), + encryptionMac: Value(Uint8List.fromList(media.encryptionMac)), + encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)), + createdAt: Value(fromTimestamp(media.timestamp)), + ), + ); - final message = await twonlyDB.messagesDao.insertMessage( - MessagesCompanion( - messageId: Value(media.senderMessageId), - senderId: Value(fromUserId), - groupId: Value(groupId), - mediaId: Value(mediaFile.mediaId), - type: Value(MessageType.media.name), - additionalMessageData: Value.absentIfNull( - media.hasAdditionalMessageData() - ? Uint8List.fromList(media.additionalMessageData) - : null, + if (mediaFile == null) { + Log.error('Could not insert media file into database'); + return; + } + + message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + messageId: Value(media.senderMessageId), + senderId: Value(fromUserId), + groupId: Value(groupId), + mediaId: Value(mediaFile!.mediaId), + type: Value(MessageType.media.name), + additionalMessageData: Value.absentIfNull( + media.hasAdditionalMessageData() + ? Uint8List.fromList(media.additionalMessageData) + : null, + ), + quotesMessageId: Value( + media.hasQuoteMessageId() ? media.quoteMessageId : null, + ), + createdAt: Value(fromTimestamp(media.timestamp)), ), - quotesMessageId: Value( - media.hasQuoteMessageId() ? media.quoteMessageId : null, - ), - createdAt: Value(fromTimestamp(media.timestamp)), - ), - ); + ); + }); + if (message != null) { - await twonlyDB.groupsDao - .increaseLastMessageExchange(groupId, fromTimestamp(media.timestamp)); - Log.info('Inserted a new media message with ID: ${message.messageId}'); + await twonlyDB.groupsDao.increaseLastMessageExchange( + groupId, + fromTimestamp(media.timestamp), + ); + Log.info('Inserted a new media message with ID: ${message!.messageId}'); await incFlameCounter( - message.groupId, + message!.groupId, true, fromTimestamp(media.timestamp), ); - unawaited(startDownloadMedia(mediaFile, false)); + unawaited(startDownloadMedia(mediaFile!, false)); } } @@ -163,8 +173,9 @@ Future handleMediaUpdate( ); return; } - final mediaFile = - await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); + final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( + message.mediaId!, + ); if (mediaFile == null) { Log.info( 'Got media file update, but media file was not found ${message.mediaId}', @@ -203,8 +214,9 @@ Future handleMediaUpdate( reuploadRequestedBy: Value(reuploadRequestedBy), ), ); - final mediaFileUpdated = - await MediaFileService.fromMediaId(mediaFile.mediaId); + final mediaFileUpdated = await MediaFileService.fromMediaId( + mediaFile.mediaId, + ); if (mediaFileUpdated != null) { if (mediaFileUpdated.uploadRequestPath.existsSync()) { mediaFileUpdated.uploadRequestPath.deleteSync(); diff --git a/lib/src/services/backup/create.backup.dart b/lib/src/services/backup/create.backup.dart index 6301a1c..97b74ed 100644 --- a/lib/src/services/backup/create.backup.dart +++ b/lib/src/services/backup/create.backup.dart @@ -10,7 +10,6 @@ import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -42,15 +41,16 @@ Future performTwonlySafeBackup({bool force = false}) async { Log.info('Starting new twonly Backup!'); - final baseDir = (await getApplicationSupportDirectory()).path; + final baseDir = globalApplicationSupportDirectory; final backupDir = Directory(join(baseDir, 'backup_twonly_safe/')); await backupDir.create(recursive: true); final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite')); - final backupDatabaseFileCleaned = - File(join(backupDir.path, 'twonly.backup.cleaned.sqlite')); + final backupDatabaseFileCleaned = File( + join(backupDir.path, 'twonly.backup.cleaned.sqlite'), + ); // copy database final originalDatabase = File(join(baseDir, 'twonly.sqlite')); @@ -70,8 +70,9 @@ Future performTwonlySafeBackup({bool force = false}) async { await backupDB.deleteDataForTwonlySafe(); - await backupDB - .customStatement('VACUUM INTO ?', [backupDatabaseFileCleaned.path]); + await backupDB.customStatement('VACUUM INTO ?', [ + backupDatabaseFileCleaned.path, + ]); await backupDB.printTableSizes(); @@ -80,10 +81,11 @@ Future performTwonlySafeBackup({bool force = false}) async { // ignore: inference_failure_on_collection_literal final secureStorageBackup = {}; const storage = FlutterSecureStorage(); - secureStorageBackup[SecureStorageKeys.signalIdentity] = - await storage.read(key: SecureStorageKeys.signalIdentity); - secureStorageBackup[SecureStorageKeys.signalSignedPreKey] = - await storage.read(key: SecureStorageKeys.signalSignedPreKey); + secureStorageBackup[SecureStorageKeys.signalIdentity] = await storage.read( + key: SecureStorageKeys.signalIdentity, + ); + secureStorageBackup[SecureStorageKeys.signalSignedPreKey] = await storage + .read(key: SecureStorageKeys.signalSignedPreKey); final userBackup = await getUser(); if (userBackup == null) return; @@ -117,13 +119,15 @@ Future performTwonlySafeBackup({bool force = false}) async { final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes); if (gUser.twonlySafeBackup!.lastBackupDone == null || - gUser.twonlySafeBackup!.lastBackupDone! - .isAfter(clock.now().subtract(const Duration(days: 90)))) { + gUser.twonlySafeBackup!.lastBackupDone!.isAfter( + clock.now().subtract(const Duration(days: 90)), + )) { force = true; } - final lastHash = - await storage.read(key: SecureStorageKeys.twonlySafeLastBackupHash); + final lastHash = await storage.read( + key: SecureStorageKeys.twonlySafeLastBackupHash, + ); if (lastHash != null && !force) { if (backupHash == lastHash) { @@ -155,8 +159,9 @@ Future performTwonlySafeBackup({bool force = false}) async { Log.info('Backup files created.'); - final encryptedBackupBytesFile = - File(join(backupDir.path, 'twonly_safe.backup')); + final encryptedBackupBytesFile = File( + join(backupDir.path, 'twonly_safe.backup'), + ); await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes); diff --git a/lib/src/services/backup/restore.backup.dart b/lib/src/services/backup/restore.backup.dart index 6f4c2d4..006d43f 100644 --- a/lib/src/services/backup/restore.backup.dart +++ b/lib/src/services/backup/restore.backup.dart @@ -8,7 +8,7 @@ import 'package:drift/drift.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; @@ -23,8 +23,10 @@ Future recoverBackup( ) async { final (backupId, encryptionKey) = await getMasterKey(password, username); - final backupServerUrl = - await getTwonlySafeBackupUrlFromServer(backupId, server); + final backupServerUrl = await getTwonlySafeBackupUrlFromServer( + backupId, + server, + ); if (backupServerUrl == null) { Log.error('Could not create backup url'); @@ -87,8 +89,9 @@ Future handleBackupData( plaintextBytes, ); - final baseDir = (await getApplicationSupportDirectory()).path; - final originalDatabase = File(join(baseDir, 'twonly.sqlite')); + final originalDatabase = File( + join(globalApplicationSupportDirectory, 'twonly.sqlite'), + ); await originalDatabase.writeAsBytes(backupContent.twonlyDatabase); const storage = FlutterSecureStorage(); diff --git a/lib/src/utils/keyvalue.dart b/lib/src/utils/keyvalue.dart index e52274d..10e62f3 100644 --- a/lib/src/utils/keyvalue.dart +++ b/lib/src/utils/keyvalue.dart @@ -1,12 +1,11 @@ import 'dart:convert'; import 'dart:io'; -import 'package:path_provider/path_provider.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/log.dart'; class KeyValueStore { static Future _getFilePath(String key) async { - final directory = await getApplicationSupportDirectory(); - return File('${directory.path}/keyvalue/$key.json'); + return File('$globalApplicationSupportDirectory/keyvalue/$key.json'); } static Future delete(String key) async { diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index f77ea5e..fcba8bb 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -1,9 +1,9 @@ +import 'dart:async'; import 'dart:io'; import 'package:clock/clock.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:twonly/globals.dart'; @@ -11,7 +11,7 @@ void initLogger() { // Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) async { - await _writeLogToFile(record); + unawaited(_writeLogToFile(record)); if (!kReleaseMode) { // ignore: avoid_print print( @@ -67,83 +67,123 @@ class Log { } Future loadLogFile() async { - final directory = await getApplicationSupportDirectory(); - final logFile = File('${directory.path}/app.log'); + return _protectFileAccess(() async { + final logFile = File('$globalApplicationSupportDirectory/app.log'); - if (logFile.existsSync()) { - return logFile.readAsString(); - } else { - return 'Log file does not exist.'; - } + if (logFile.existsSync()) { + return logFile.readAsString(); + } else { + return 'Log file does not exist.'; + } + }); } Future readLast1000Lines() async { - final dir = await getApplicationSupportDirectory(); - final file = File('${dir.path}/app.log'); - if (!file.existsSync()) return ''; - final all = await file.readAsLines(); - final start = all.length > 1000 ? all.length - 1000 : 0; - return all.sublist(start).join('\n'); + return _protectFileAccess(() async { + final file = File('$globalApplicationSupportDirectory/app.log'); + if (!file.existsSync()) return ''; + final all = await file.readAsLines(); + final start = all.length > 1000 ? all.length - 1000 : 0; + return all.sublist(start).join('\n'); + }); } -Mutex sameProcessProtection = Mutex(); +final Mutex _logMutex = Mutex(); + +Future _protectFileAccess(Future Function() action) async { + return _logMutex.protect(() async { + final lockFile = File('$globalApplicationSupportDirectory/app.log.lock'); + var lockAcquired = false; + + while (!lockAcquired) { + try { + lockFile.createSync(exclusive: true); + lockAcquired = true; + } on FileSystemException catch (e) { + final isExists = e is PathExistsException || e.osError?.errorCode == 17; + if (!isExists) { + break; + } + try { + final stat = lockFile.statSync(); + if (stat.type != FileSystemEntityType.notFound) { + final age = DateTime.now().difference(stat.modified).inMilliseconds; + if (age > 1000) { + lockFile.deleteSync(); + continue; + } + } + } catch (_) {} + await Future.delayed(const Duration(milliseconds: 50)); + } catch (_) { + break; + } + } + try { + return await action(); + } finally { + if (lockAcquired) { + try { + if (lockFile.existsSync()) { + lockFile.deleteSync(); + } + } catch (_) {} + } + } + }); +} Future _writeLogToFile(LogRecord record) async { - final directory = await getApplicationSupportDirectory(); - final logFile = File('${directory.path}/app.log'); - if (!logFile.existsSync()) { - logFile.createSync(recursive: true); - } + final logFile = File('$globalApplicationSupportDirectory/app.log'); final logMessage = '${clock.now().toString().split(".")[0]} ${record.level.name} [twonly] ${record.loggerName} > ${record.message}\n'; - // > Note that this does not actually lock the file for access. Also note that advisory locks are on a process level. - // > This means that several isolates in the same process can obtain an exclusive lock on the same file. - return sameProcessProtection.protect(() async { + return _protectFileAccess(() async { + if (!logFile.existsSync()) { + logFile.createSync(recursive: true); + } final raf = await logFile.open(mode: FileMode.writeOnlyAppend); - try { - // Use FileLock.blockingExclusive to wait until the lock is available - await raf.lock(FileLock.blockingExclusive); await raf.writeString(logMessage); await raf.flush(); } catch (e) { // ignore: avoid_print print('Error during file access: $e'); } finally { - await raf.unlock(); await raf.close(); } }); } Future cleanLogFile() async { - final directory = await getApplicationSupportDirectory(); - final logFile = File('${directory.path}/app.log'); + return _protectFileAccess(() async { + final logFile = File('$globalApplicationSupportDirectory/app.log'); - if (logFile.existsSync()) { - final lines = await logFile.readAsLines(); + if (logFile.existsSync()) { + final lines = await logFile.readAsLines(); - if (lines.length <= 10000) return; + if (lines.length <= 10000) return; - final removeCount = lines.length - 10000; - final remaining = lines.sublist(removeCount, lines.length); + final removeCount = lines.length - 10000; + final remaining = lines.sublist(removeCount, lines.length); - final sink = logFile.openWrite()..writeAll(remaining, '\n'); - await sink.close(); - } + final sink = logFile.openWrite()..writeAll(remaining, '\n'); + await sink.close(); + } + }); } Future deleteLogFile() async { - final directory = await getApplicationSupportDirectory(); - final logFile = File('${directory.path}/app.log'); + return _protectFileAccess(() async { + final logFile = File('$globalApplicationSupportDirectory/app.log'); - if (logFile.existsSync()) { - await logFile.delete(); - return true; - } - return false; + if (logFile.existsSync()) { + await logFile.delete(); + return true; + } + return false; + }); } String _getCallerSourceCodeFilename() { @@ -159,8 +199,11 @@ String _getCallerSourceCodeFilename() { lineNumber = parts.last.split(':')[1]; // Extract the line number } else { final firstLine = stackTraceString.split('\n')[0]; - fileName = - firstLine.split('/').last.split(':').first; // Extract the file name + fileName = firstLine + .split('/') + .last + .split(':') + .first; // Extract the file name lineNumber = firstLine.split(':')[1]; // Extract the line number } lineNumber = lineNumber.replaceAll(')', ''); diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index 73d1cb0..1aedc31 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -88,9 +88,8 @@ class MainCameraController { scannedUrl = null; try { await cameraController?.stopImageStream(); - } catch (e) { - Log.warn(e); - } + // ignore: empty_catches + } catch (e) {} final cameraControllerTemp = cameraController; cameraController = null; // prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.) @@ -166,7 +165,8 @@ class MainCameraController { selectedCameraDetails.minAvailableZoom = await cameraController?.getMinZoomLevel() ?? 1; selectedCameraDetails - ..isZoomAble = selectedCameraDetails.maxAvailableZoom != + ..isZoomAble = + selectedCameraDetails.maxAvailableZoom != selectedCameraDetails.minAvailableZoom ..cameraLoaded = true ..cameraId = cameraId; @@ -323,8 +323,9 @@ class MainCameraController { customPaint = CustomPaint(painter: painter); if (barcodes.isEmpty && timeSharedLinkWasSetWithQr != null) { - if (timeSharedLinkWasSetWithQr! - .isAfter(DateTime.now().subtract(const Duration(seconds: 2)))) { + if (timeSharedLinkWasSetWithQr!.isAfter( + DateTime.now().subtract(const Duration(seconds: 2)), + )) { setSharedLinkForPreview(null); } } @@ -376,8 +377,8 @@ class MainCameraController { content: Text( globalRootScaffoldMessengerKey.currentContext?.lang .verifiedPublicKey( - getContactDisplayName(contact), - ) ?? + getContactDisplayName(contact), + ) ?? '', ), duration: const Duration(seconds: 6), diff --git a/pubspec.yaml b/pubspec.yaml index dd62e87..bddd5a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.0.97+97 +version: 0.0.99+99 environment: sdk: ^3.11.0 diff --git a/test/features/link_parser_test.dart b/test/features/link_parser_test.dart index ea637c7..d5f3c21 100644 --- a/test/features/link_parser_test.dart +++ b/test/features/link_parser_test.dart @@ -38,7 +38,7 @@ void main() { image: 'https://files.mastodon.social/media_attachments/files/115/883/317/526/523/824/original/6fa7ef90ec68f1f1.jpg', vendor: Vendor.mastodonSocialMediaPosting, - shareAction: 90, + shareAction: 80, likeAction: 290, ), LinkParserTest(