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
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.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;
_ctrl.clear();
setState(() => _canSend = false);
await widget.room.sendTextEvent(
text,
inReplyTo: widget.replyTo,
);
await widget.room.sendTextEvent(text, inReplyTo: widget.replyTo);
widget.onClearReply?.call();
}
@@ -59,21 +55,30 @@ class _ChatInputState extends State<ChatInput> {
defaultTargetPlatform == TargetPlatform.linux ||
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(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.replyTo != null) _ReplyBar(
event: widget.replyTo!,
onClear: widget.onClearReply ?? () {},
),
Padding(
padding: EdgeInsets.fromLTRB(
isDesktop ? 12 : 8,
isDesktop ? 10 : 4,
isDesktop ? 12 : 8,
isDesktop ? 14 : 8,
if (widget.replyTo != null)
_ReplyBar(
event: widget.replyTo!,
onClear: widget.onClearReply ?? () {},
),
Padding(
padding: outerPad,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
@@ -83,19 +88,24 @@ class _ChatInputState extends State<ChatInput> {
minLines: 1,
maxLines: 6,
textCapitalization: TextCapitalization.sentences,
style: TextStyle(fontSize: isDesktop ? 16 : 14),
style: TextStyle(fontSize: fontSize),
decoration: InputDecoration(
hintText: 'Nachricht...',
hintStyle: TextStyle(
fontSize: isDesktop ? 16 : 14,
color: theme.colorScheme.onSurface.withAlpha(100),
),
hintStyle: TextStyle(fontSize: fontSize),
contentPadding: EdgeInsets.symmetric(
horizontal: isDesktop ? 22 : 16,
vertical: isDesktop ? 20 : 10,
horizontal: hPad,
vertical: vPad,
),
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,
),
filled: true,
@@ -113,18 +123,18 @@ class _ChatInputState extends State<ChatInput> {
onTap: _canSend ? _send : null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: isDesktop ? 60 : 44,
height: isDesktop ? 60 : 44,
width: btnSize,
height: btnSize,
decoration: BoxDecoration(
color: _canSend
? theme.colorScheme.primary
: theme.colorScheme.primary.withAlpha(70),
borderRadius: BorderRadius.circular(isDesktop ? 16 : 14),
borderRadius: BorderRadius.circular(btnRadius),
),
child: Center(
child: Icon(
Icons.send_rounded,
size: isDesktop ? 26 : 20,
size: iconSize,
color: Colors.white,
),
),
+19 -18
View File
@@ -1,9 +1,9 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
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_provider/path_provider.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 {
setState(() => _state = _DownloadState.installing);
if (Platform.isWindows) {
await Process.start(file.path, [], runInShell: false);
if (mounted) Navigator.of(context).pop();
exit(0);
} else if (Platform.isAndroid) {
final result = await OpenFile.open(
file.path,
type: 'application/vnd.android.package-archive',
);
try {
if (Platform.isWindows) {
await Process.start(file.path, [], runInShell: false);
if (mounted) Navigator.of(context).pop();
exit(0);
} else if (Platform.isAndroid) {
await _installChannel
.invokeMethod('installApk', {'path': file.path});
if (mounted) setState(() => _state = _DownloadState.done);
}
} catch (e) {
if (mounted) {
if (result.type == ResultType.done) {
setState(() => _state = _DownloadState.done);
} else {
setState(() {
_state = _DownloadState.error;
_errorMsg = result.message;
});
}
setState(() {
_state = _DownloadState.error;
_errorMsg = e.toString().split('\n').first;
});
}
}
}
-8
View File
@@ -757,14 +757,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
-1
View File
@@ -57,7 +57,6 @@ dependencies:
# File handling
file_picker: ^8.1.7
desktop_drop: ^0.4.4
open_file_plus: ^3.4.1
# Media playback
media_kit: ^1.1.11