fix: replace open_file_plus with native Kotlin MethodChannel for APK install

open_file_plus 3.4.1 uses deprecated PluginRegistry.Registrar API incompatible
with current Flutter. Replaced with a MethodChannel in MainActivity.kt that calls
FileProvider.getUriForFile() and launches the system package installer directly.

Also rework chat_input: 4px border radius on desktop, vPad 18px, 56px send button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bernd Steckmeister
2026-04-28 15:44:10 +02:00
parent cdf48c4d45
commit 238fc3f670
5 changed files with 103 additions and 55 deletions
@@ -1,5 +1,51 @@
package chat.pyramid.pyramid package chat.pyramid.pyramid
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.core.content.FileProvider
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
class MainActivity : FlutterActivity() class MainActivity : FlutterActivity() {
private val channel = "chat.pyramid.pyramid/install"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel)
.setMethodCallHandler { call, result ->
if (call.method == "installApk") {
val path = call.argument<String>("path")
if (path == null) {
result.error("INVALID_ARG", "path is null", null)
return@setMethodCallHandler
}
try {
val file = File(path)
val uri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(
this,
"${applicationContext.packageName}.fileprovider",
file
)
} else {
Uri.fromFile(file)
}
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(intent)
result.success(null)
} catch (e: Exception) {
result.error("INSTALL_ERROR", e.message, null)
}
} else {
result.notImplemented()
}
}
}
}
+37 -27
View File
@@ -43,11 +43,7 @@ class _ChatInputState extends State<ChatInput> {
if (text.isEmpty) return; if (text.isEmpty) return;
_ctrl.clear(); _ctrl.clear();
setState(() => _canSend = false); setState(() => _canSend = false);
await widget.room.sendTextEvent(text, inReplyTo: widget.replyTo);
await widget.room.sendTextEvent(
text,
inReplyTo: widget.replyTo,
);
widget.onClearReply?.call(); widget.onClearReply?.call();
} }
@@ -59,21 +55,30 @@ class _ChatInputState extends State<ChatInput> {
defaultTargetPlatform == TargetPlatform.linux || defaultTargetPlatform == TargetPlatform.linux ||
defaultTargetPlatform == TargetPlatform.macOS); defaultTargetPlatform == TargetPlatform.macOS);
// Desktop: taller bar, 4px square corners, bigger button
// Mobile: compact bar, pill shape, smaller button
final fieldRadius = isDesktop ? 4.0 : 20.0;
final btnSize = isDesktop ? 56.0 : 44.0;
final btnRadius = isDesktop ? 6.0 : 12.0;
final iconSize = isDesktop ? 24.0 : 20.0;
final fontSize = isDesktop ? 15.0 : 14.0;
final vPad = isDesktop ? 18.0 : 10.0;
final hPad = isDesktop ? 16.0 : 14.0;
final outerPad = isDesktop
? const EdgeInsets.fromLTRB(12, 10, 12, 12)
: const EdgeInsets.fromLTRB(8, 4, 8, 8);
return SafeArea( return SafeArea(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (widget.replyTo != null) _ReplyBar( if (widget.replyTo != null)
event: widget.replyTo!, _ReplyBar(
onClear: widget.onClearReply ?? () {}, event: widget.replyTo!,
), onClear: widget.onClearReply ?? () {},
Padding(
padding: EdgeInsets.fromLTRB(
isDesktop ? 12 : 8,
isDesktop ? 10 : 4,
isDesktop ? 12 : 8,
isDesktop ? 14 : 8,
), ),
Padding(
padding: outerPad,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
@@ -83,19 +88,24 @@ class _ChatInputState extends State<ChatInput> {
minLines: 1, minLines: 1,
maxLines: 6, maxLines: 6,
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
style: TextStyle(fontSize: isDesktop ? 16 : 14), style: TextStyle(fontSize: fontSize),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Nachricht...', hintText: 'Nachricht...',
hintStyle: TextStyle( hintStyle: TextStyle(fontSize: fontSize),
fontSize: isDesktop ? 16 : 14,
color: theme.colorScheme.onSurface.withAlpha(100),
),
contentPadding: EdgeInsets.symmetric( contentPadding: EdgeInsets.symmetric(
horizontal: isDesktop ? 22 : 16, horizontal: hPad,
vertical: isDesktop ? 20 : 10, vertical: vPad,
), ),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(isDesktop ? 16 : 24), borderRadius: BorderRadius.circular(fieldRadius),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(fieldRadius),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(fieldRadius),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
filled: true, filled: true,
@@ -113,18 +123,18 @@ class _ChatInputState extends State<ChatInput> {
onTap: _canSend ? _send : null, onTap: _canSend ? _send : null,
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),
width: isDesktop ? 60 : 44, width: btnSize,
height: isDesktop ? 60 : 44, height: btnSize,
decoration: BoxDecoration( decoration: BoxDecoration(
color: _canSend color: _canSend
? theme.colorScheme.primary ? theme.colorScheme.primary
: theme.colorScheme.primary.withAlpha(70), : theme.colorScheme.primary.withAlpha(70),
borderRadius: BorderRadius.circular(isDesktop ? 16 : 14), borderRadius: BorderRadius.circular(btnRadius),
), ),
child: Center( child: Center(
child: Icon( child: Icon(
Icons.send_rounded, Icons.send_rounded,
size: isDesktop ? 26 : 20, size: iconSize,
color: Colors.white, color: Colors.white,
), ),
), ),
+19 -18
View File
@@ -1,9 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:open_file_plus/open_file_plus.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:pyramid/core/theme.dart'; import 'package:pyramid/core/theme.dart';
@@ -111,27 +111,28 @@ class _UpdateDownloadDialogState extends ConsumerState<_UpdateDownloadDialog>
} }
} }
static const _installChannel =
MethodChannel('chat.pyramid.pyramid/install');
Future<void> _install(File file) async { Future<void> _install(File file) async {
setState(() => _state = _DownloadState.installing); setState(() => _state = _DownloadState.installing);
if (Platform.isWindows) { try {
await Process.start(file.path, [], runInShell: false); if (Platform.isWindows) {
if (mounted) Navigator.of(context).pop(); await Process.start(file.path, [], runInShell: false);
exit(0); if (mounted) Navigator.of(context).pop();
} else if (Platform.isAndroid) { exit(0);
final result = await OpenFile.open( } else if (Platform.isAndroid) {
file.path, await _installChannel
type: 'application/vnd.android.package-archive', .invokeMethod('installApk', {'path': file.path});
); if (mounted) setState(() => _state = _DownloadState.done);
}
} catch (e) {
if (mounted) { if (mounted) {
if (result.type == ResultType.done) { setState(() {
setState(() => _state = _DownloadState.done); _state = _DownloadState.error;
} else { _errorMsg = e.toString().split('\n').first;
setState(() { });
_state = _DownloadState.error;
_errorMsg = result.message;
});
}
} }
} }
} }
-8
View File
@@ -757,14 +757,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.3.0" version: "9.3.0"
open_file_plus:
dependency: "direct main"
description:
name: open_file_plus
sha256: "01ad7011d46d253aa22e5f0bc7345ca49cfe652c50ae9b0af945491c1728a8c3"
url: "https://pub.dev"
source: hosted
version: "3.4.1+1"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
-1
View File
@@ -57,7 +57,6 @@ dependencies:
# File handling # File handling
file_picker: ^8.1.7 file_picker: ^8.1.7
desktop_drop: ^0.4.4 desktop_drop: ^0.4.4
open_file_plus: ^3.4.1
# Media playback # Media playback
media_kit: ^1.1.11 media_kit: ^1.1.11