fix: handle app upgrade crashes from incompatible data and foreground service receivers (#312)
* fix: handle app upgrade crashes from incompatible data and foreground service When upgrading from an older version, multiple issues caused crashes: 1. The Rust whitenoise crate's secrets store format changed, causing "Secrets store error: Key not found" during initialization. Since the Rust singleton (tokio::sync::OnceCell) can't be reinitialized after partial failure, a version marker file now detects stale data and wipes the data directory before Rust init runs. 2. The flutter_foreground_task plugin stored foregroundServiceType 0x201 (dataSync|shortService) in native SharedPreferences, but the manifest only declares dataSync (0x01). Android rejected startForeground() with IllegalArgumentException. A one-time cleanup in MainActivity.onCreate() clears these native prefs. 3. FlutterSecureStorage's internal migration from EncryptedSharedPreferences re-introduced the old active_account_pubkey after deleteAll(). Fixed by calling readAll() first to trigger the migration, then deleteAll(). 4. The plugin's RebootReceiver and RestartReceiver caused cascade crashes on package update. Both are now disabled via manifest merge overrides. * chore: remove stale url_launcher from generated plugin files and add build improvements Remove url_launcher references from Linux/Windows plugin registrants (dependency was already removed from pubspec.yaml). Add split APK, AAB build targets and quiet spinner to OpenSSL build script.
This commit is contained in:
@@ -73,6 +73,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix app crashes on upgrade from older versions by wiping incompatible data before Rust initialization [PR #312](https://github.com/marmot-protocol/whitenoise/pull/312)
|
||||
- Disable foreground task plugin receivers to prevent crashes on package update [PR #312](https://github.com/marmot-protocol/whitenoise/pull/312)
|
||||
- Adds internet permission in android manifest [PR #7](https://github.com/marmot-protocol/sloth/pull/7)
|
||||
- Fixes logout not working after app reinstall [PR #31](https://github.com/marmot-protocol/sloth/pull/31)
|
||||
- Fixes sign out exception and adds dedicated sign out screen with private key backup [PR #45](https://github.com/marmot-protocol/sloth/pull/45)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
@@ -42,6 +43,17 @@
|
||||
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
<!-- Disable the plugin's receivers to prevent crashes on package update
|
||||
and service restart. The foreground service is managed in the app's
|
||||
normal startup flow instead. -->
|
||||
<receiver
|
||||
android:name="com.pravera.flutter_foreground_task.service.RebootReceiver"
|
||||
android:enabled="false"
|
||||
tools:node="replace" />
|
||||
<receiver
|
||||
android:name="com.pravera.flutter_foreground_task.service.RestartReceiver"
|
||||
android:enabled="false"
|
||||
tools:node="replace" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
|
||||
@@ -1,13 +1,44 @@
|
||||
package org.parres.whitenoise
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import io.crates.keyring.Keyring
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
companion object {
|
||||
// TODO: Remove migration cleanup in the next release.
|
||||
private const val MIGRATION_PREFS = "org.parres.whitenoise.migration"
|
||||
private const val KEY_CLEANED_FGS_PREFS = "cleaned_fgs_prefs_v1"
|
||||
|
||||
private val FOREGROUND_TASK_PREFS = listOf(
|
||||
"com.pravera.flutter_foreground_task.prefs.FOREGROUND_SERVICE_STATUS",
|
||||
"com.pravera.flutter_foreground_task.prefs.FOREGROUND_SERVICE_TYPES",
|
||||
"com.pravera.flutter_foreground_task.prefs.FOREGROUND_TASK_OPTIONS",
|
||||
"com.pravera.flutter_foreground_task.prefs.NOTIFICATION_OPTIONS",
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
cleanForegroundTaskPrefs()
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
Keyring.initializeNdkContext(applicationContext)
|
||||
flutterEngine.plugins.add(AndroidSignerPlugin())
|
||||
}
|
||||
|
||||
private fun cleanForegroundTaskPrefs() {
|
||||
val migrationPrefs = getSharedPreferences(MIGRATION_PREFS, Context.MODE_PRIVATE)
|
||||
if (migrationPrefs.getBoolean(KEY_CLEANED_FGS_PREFS, false)) return
|
||||
|
||||
for (prefsName in FOREGROUND_TASK_PREFS) {
|
||||
getSharedPreferences(prefsName, Context.MODE_PRIVATE).edit().clear().commit()
|
||||
}
|
||||
|
||||
migrationPrefs.edit().putBoolean(KEY_CLEANED_FGS_PREFS, true).commit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,12 +258,25 @@ build-android-quiet:
|
||||
build-android-apk flavor:
|
||||
./scripts/build_android.sh && flutter build apk --flavor {{flavor}}
|
||||
|
||||
# Build a fat APK (all ABIs in one file)
|
||||
build-production-apk:
|
||||
./scripts/build_android.sh && flutter build apk --flavor production
|
||||
|
||||
build-staging-apk:
|
||||
./scripts/build_android.sh && flutter build apk --flavor staging
|
||||
|
||||
# Build per-ABI split APKs (separate .apk per architecture)
|
||||
build-split-apk flavor="production":
|
||||
./scripts/build_android.sh && flutter build apk --flavor {{flavor}} --split-per-abi
|
||||
|
||||
# Build an Android App Bundle (per-ABI splitting handled by Play Store)
|
||||
build-aab flavor="production":
|
||||
./scripts/build_android.sh && flutter build appbundle --flavor {{flavor}}
|
||||
|
||||
# Release builds
|
||||
build-release-apk: (build-split-apk "production")
|
||||
build-release-aab: (build-aab "production")
|
||||
|
||||
when-apk: build-staging-apk
|
||||
|
||||
# Run the app on a connected device (staging flavor by default)
|
||||
|
||||
+39
-16
@@ -1,10 +1,11 @@
|
||||
import 'dart:io' show Directory;
|
||||
import 'dart:io' show Directory, File, FileSystemException;
|
||||
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show DeviceOrientation, SystemChrome;
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart' show FlutterForegroundTask;
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart' show ScreenUtilInit;
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart' show FlutterSecureStorage;
|
||||
import 'package:go_router/go_router.dart' show GoRouter;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart'
|
||||
show ConsumerStatefulWidget, ConsumerState, ProviderContainer, UncontrolledProviderScope;
|
||||
@@ -19,7 +20,9 @@ import 'package:whitenoise/src/rust/api.dart' as rust_api;
|
||||
import 'package:whitenoise/src/rust/frb_generated.dart';
|
||||
import 'package:whitenoise/theme.dart';
|
||||
|
||||
const kUnencryptedDatabaseError = 'database was created without encryption';
|
||||
// TODO: Remove migration gate and related code in the next release.
|
||||
const kDataVersion = 1;
|
||||
const kDataVersionFile = 'data_version';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -38,26 +41,46 @@ Future<ProviderContainer> initializeAppContainer() async {
|
||||
final logsDir = '${dir.path}/whitenoise/logs';
|
||||
await Directory(dataDir).create(recursive: true);
|
||||
await Directory(logsDir).create(recursive: true);
|
||||
final config = await rust_api.createWhitenoiseConfig(dataDir: dataDir, logsDir: logsDir);
|
||||
|
||||
try {
|
||||
await rust_api.initializeWhitenoise(config: config);
|
||||
} catch (e) {
|
||||
if (!e.toString().contains(kUnencryptedDatabaseError)) {
|
||||
rethrow;
|
||||
}
|
||||
final envDir = Directory('$dataDir/${kDebugMode ? 'dev' : 'release'}');
|
||||
if (envDir.existsSync()) {
|
||||
await envDir.delete(recursive: true);
|
||||
}
|
||||
await rust_api.initializeWhitenoise(config: config);
|
||||
}
|
||||
await _migrateDataIfNeeded(dataDir);
|
||||
|
||||
final config = await rust_api.createWhitenoiseConfig(dataDir: dataDir, logsDir: logsDir);
|
||||
await rust_api.initializeWhitenoise(config: config);
|
||||
|
||||
final container = ProviderContainer();
|
||||
await container.read(authProvider.future);
|
||||
return container;
|
||||
}
|
||||
|
||||
Future<void> _migrateDataIfNeeded(String dataDir) async {
|
||||
final versionFile = File('$dataDir/$kDataVersionFile');
|
||||
int? currentVersion;
|
||||
try {
|
||||
if (versionFile.existsSync()) {
|
||||
currentVersion = int.tryParse(versionFile.readAsStringSync().trim());
|
||||
}
|
||||
} on FileSystemException {
|
||||
// Corrupt or unreadable file — treat as no version.
|
||||
}
|
||||
|
||||
if (currentVersion == kDataVersion) return;
|
||||
|
||||
final dataDirObj = Directory(dataDir);
|
||||
if (dataDirObj.existsSync()) {
|
||||
await dataDirObj.delete(recursive: true);
|
||||
await dataDirObj.create(recursive: true);
|
||||
}
|
||||
|
||||
// Read triggers the internal migration from EncryptedSharedPreferences to
|
||||
// the new cipher storage. Then deleteAll clears everything including any
|
||||
// keys the migration re-introduced from the old app.
|
||||
const secureStorage = FlutterSecureStorage();
|
||||
await secureStorage.readAll();
|
||||
await secureStorage.deleteAll();
|
||||
await FlutterForegroundTask.clearAllData();
|
||||
versionFile.writeAsStringSync('$kDataVersion');
|
||||
}
|
||||
|
||||
class WnApp extends ConsumerStatefulWidget {
|
||||
const WnApp({super.key});
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <emoji_picker_flutter/emoji_picker_flutter_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar =
|
||||
@@ -21,7 +20,4 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
emoji_picker_flutter
|
||||
file_selector_linux
|
||||
flutter_secure_storage_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -17,6 +17,38 @@ print_error() {
|
||||
echo -e "\033[1;31m$1\033[0m"
|
||||
}
|
||||
|
||||
# Spinner for long-running commands
|
||||
# Usage: run_quiet "description" logfile command [args...]
|
||||
run_quiet() {
|
||||
local desc="$1"
|
||||
local logfile="$2"
|
||||
shift 2
|
||||
|
||||
printf " %-40s " "$desc"
|
||||
|
||||
# Run command in background with output redirected to log file
|
||||
"$@" >> "$logfile" 2>&1 &
|
||||
local pid=$!
|
||||
local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
||||
local i=0
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
printf "\b%s" "${spin:i++%${#spin}:1}"
|
||||
sleep 0.1
|
||||
done
|
||||
wait "$pid"
|
||||
local exit_code=$?
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
printf "\b\033[1;32m✓\033[0m\n"
|
||||
else
|
||||
printf "\b\033[1;31m✗\033[0m\n"
|
||||
print_error "Command failed. Last 20 lines of log:"
|
||||
tail -20 "$logfile"
|
||||
print_error "Full log: $logfile"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Configuration
|
||||
OPENSSL_VERSION="3.4.1"
|
||||
OPENSSL_SHA256="002a2d6b30b58bf4bea46c43bdd96365aaf8daa6c428782aa4feee06da197df3"
|
||||
@@ -86,33 +118,40 @@ build_openssl_for_target() {
|
||||
|
||||
# Clean and re-extract source for each target (OpenSSL doesn't support out-of-tree builds well)
|
||||
local build_src="$OPENSSL_BUILD_DIR/build-$rust_target"
|
||||
local logfile="$OPENSSL_BUILD_DIR/$rust_target.log"
|
||||
rm -rf "$build_src"
|
||||
: > "$logfile"
|
||||
cp -r "$OPENSSL_SRC_DIR" "$build_src"
|
||||
cd "$build_src"
|
||||
|
||||
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
||||
export PATH="$TOOLCHAIN/bin:$PATH"
|
||||
|
||||
./Configure "$openssl_target" \
|
||||
-D__ANDROID_API__=$ANDROID_API \
|
||||
--prefix="$install_prefix" \
|
||||
--openssldir="$install_prefix/ssl" \
|
||||
no-shared \
|
||||
no-tests \
|
||||
no-ui-console \
|
||||
no-stdio \
|
||||
-fPIC
|
||||
run_quiet "Configuring ($openssl_target)..." "$logfile" \
|
||||
./Configure "$openssl_target" \
|
||||
-D__ANDROID_API__=$ANDROID_API \
|
||||
--prefix="$install_prefix" \
|
||||
--openssldir="$install_prefix/ssl" \
|
||||
no-shared \
|
||||
no-tests \
|
||||
no-ui-console \
|
||||
no-stdio \
|
||||
-fPIC
|
||||
|
||||
make -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu)" build_libs
|
||||
make install_dev
|
||||
run_quiet "Compiling..." "$logfile" \
|
||||
make -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu)" build_libs
|
||||
|
||||
run_quiet "Installing headers & libs..." "$logfile" \
|
||||
make install_dev
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
rm -rf "$build_src"
|
||||
|
||||
if [ -f "$install_prefix/lib/libcrypto.a" ]; then
|
||||
print_success "Built OpenSSL for $rust_target"
|
||||
print_success " Built OpenSSL for $rust_target"
|
||||
else
|
||||
print_error "Failed to build OpenSSL for $rust_target"
|
||||
print_error "Check log: $logfile"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -125,7 +164,7 @@ if [ ! -d "$OPENSSL_SRC_DIR" ]; then
|
||||
|
||||
TARBALL="openssl-$OPENSSL_VERSION.tar.gz"
|
||||
if [ ! -f "$TARBALL" ]; then
|
||||
curl -LO "https://github.com/openssl/openssl/releases/download/openssl-$OPENSSL_VERSION/$TARBALL"
|
||||
curl -L --progress-bar -o "$TARBALL" "https://github.com/openssl/openssl/releases/download/openssl-$OPENSSL_VERSION/$TARBALL"
|
||||
fi
|
||||
|
||||
print_step "Verifying tarball integrity"
|
||||
|
||||
+59
-34
@@ -5,7 +5,8 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart'
|
||||
show AsyncData, ProviderContainer, ProviderScope;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:whitenoise/main.dart' show WnApp, initializeAppContainer, kUnencryptedDatabaseError;
|
||||
import 'package:whitenoise/main.dart'
|
||||
show WnApp, initializeAppContainer, kDataVersion, kDataVersionFile;
|
||||
import 'package:whitenoise/providers/auth_provider.dart';
|
||||
import 'package:whitenoise/providers/theme_provider.dart';
|
||||
import 'package:whitenoise/src/rust/api.dart' as rust_api;
|
||||
@@ -89,8 +90,6 @@ class _MockInitApi extends MockWnApi {
|
||||
String? createdConfigLogsDir;
|
||||
rust_api.WhitenoiseConfig? initializedConfig;
|
||||
int initCallCount = 0;
|
||||
int failFirstNCalls = 0;
|
||||
String failMessage = kUnencryptedDatabaseError;
|
||||
|
||||
@override
|
||||
Future<rust_api.WhitenoiseConfig> crateApiCreateWhitenoiseConfig({
|
||||
@@ -107,9 +106,6 @@ class _MockInitApi extends MockWnApi {
|
||||
required rust_api.WhitenoiseConfig config,
|
||||
}) async {
|
||||
initCallCount++;
|
||||
if (initCallCount <= failFirstNCalls) {
|
||||
throw Exception(failMessage);
|
||||
}
|
||||
initializedConfig = config;
|
||||
}
|
||||
|
||||
@@ -120,8 +116,6 @@ class _MockInitApi extends MockWnApi {
|
||||
createdConfigLogsDir = null;
|
||||
initializedConfig = null;
|
||||
initCallCount = 0;
|
||||
failFirstNCalls = 0;
|
||||
failMessage = kUnencryptedDatabaseError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,11 +186,19 @@ void main() {
|
||||
setUp(() {
|
||||
pathProvider = _mockPathProvider();
|
||||
resetSecureStorage = _mockSecureStorage();
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
const MethodChannel('plugins.flutter.io/shared_preferences'),
|
||||
(call) async => <String, Object>{},
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
pathProvider.reset();
|
||||
resetSecureStorage();
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
const MethodChannel('plugins.flutter.io/shared_preferences'),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('creates data directory', () async {
|
||||
@@ -241,38 +243,61 @@ void main() {
|
||||
expect(container.read(authProvider), isA<AsyncData>());
|
||||
});
|
||||
|
||||
test('wipes env data directory and retries on unencrypted db error', () async {
|
||||
mockApi.failFirstNCalls = 1;
|
||||
final envDir = Directory('${pathProvider.tempDir.path}/whitenoise/data/dev');
|
||||
final marker = File('${envDir.path}/whitenoise.sqlite');
|
||||
await envDir.create(recursive: true);
|
||||
test('writes version file on fresh install', () async {
|
||||
await initializeAppContainer();
|
||||
|
||||
final versionFile = File('${pathProvider.tempDir.path}/whitenoise/data/$kDataVersionFile');
|
||||
expect(versionFile.existsSync(), isTrue);
|
||||
expect(versionFile.readAsStringSync().trim(), '$kDataVersion');
|
||||
});
|
||||
|
||||
test('skips migration when version matches', () async {
|
||||
final dataDir = Directory('${pathProvider.tempDir.path}/whitenoise/data');
|
||||
await dataDir.create(recursive: true);
|
||||
final versionFile = File('${dataDir.path}/$kDataVersionFile');
|
||||
versionFile.writeAsStringSync('$kDataVersion');
|
||||
final marker = File('${dataDir.path}/whitenoise.json');
|
||||
await marker.create();
|
||||
|
||||
await initializeAppContainer();
|
||||
|
||||
expect(marker.existsSync(), isTrue);
|
||||
});
|
||||
|
||||
final container = await initializeAppContainer();
|
||||
test('wipes data directory when no version file exists', () async {
|
||||
final dataDir = Directory('${pathProvider.tempDir.path}/whitenoise/data');
|
||||
await dataDir.create(recursive: true);
|
||||
final oldSecrets = File('${dataDir.path}/whitenoise.json');
|
||||
final oldUuid = File('${dataDir.path}/whitenoise_uuid');
|
||||
final oldDb = File('${dataDir.path}/release/whitenoise.sqlite');
|
||||
await Directory('${dataDir.path}/release').create(recursive: true);
|
||||
await oldSecrets.create();
|
||||
await oldUuid.create();
|
||||
await oldDb.create();
|
||||
|
||||
await initializeAppContainer();
|
||||
|
||||
expect(oldSecrets.existsSync(), isFalse);
|
||||
expect(oldUuid.existsSync(), isFalse);
|
||||
expect(oldDb.existsSync(), isFalse);
|
||||
expect(dataDir.existsSync(), isTrue);
|
||||
final versionFile = File('${dataDir.path}/$kDataVersionFile');
|
||||
expect(versionFile.existsSync(), isTrue);
|
||||
expect(versionFile.readAsStringSync().trim(), '$kDataVersion');
|
||||
});
|
||||
|
||||
test('wipes data directory when version is outdated', () async {
|
||||
final dataDir = Directory('${pathProvider.tempDir.path}/whitenoise/data');
|
||||
await dataDir.create(recursive: true);
|
||||
final versionFile = File('${dataDir.path}/$kDataVersionFile');
|
||||
versionFile.writeAsStringSync('0');
|
||||
final marker = File('${dataDir.path}/whitenoise.json');
|
||||
await marker.create();
|
||||
|
||||
await initializeAppContainer();
|
||||
|
||||
expect(container, isA<ProviderContainer>());
|
||||
expect(marker.existsSync(), isFalse);
|
||||
expect(mockApi.initCallCount, 2);
|
||||
});
|
||||
|
||||
test('rethrows unencrypted db error when retry also fails', () async {
|
||||
mockApi.failFirstNCalls = 2;
|
||||
|
||||
await expectLater(initializeAppContainer(), throwsException);
|
||||
});
|
||||
|
||||
test('rethrows unrelated errors without wiping', () async {
|
||||
mockApi.failFirstNCalls = 1;
|
||||
mockApi.failMessage = 'network timeout';
|
||||
final envDir = Directory('${pathProvider.tempDir.path}/whitenoise/data/dev');
|
||||
final marker = File('${envDir.path}/whitenoise.sqlite');
|
||||
await envDir.create(recursive: true);
|
||||
await marker.create();
|
||||
|
||||
await expectLater(initializeAppContainer(), throwsException);
|
||||
expect(marker.existsSync(), isTrue);
|
||||
expect(versionFile.readAsStringSync().trim(), '$kDataVersion');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
EmojiPickerFlutterPluginCApiRegisterWithRegistrar(
|
||||
@@ -21,6 +20,4 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
permission_handler_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
Reference in New Issue
Block a user